diff --git a/apps/error-tracking/README.md b/apps/error-tracking/README.md new file mode 100644 index 000000000..484264974 --- /dev/null +++ b/apps/error-tracking/README.md @@ -0,0 +1,10 @@ +# Error tracking test apps + +Fixtures for `wizard error-tracking`. On every supported app the run is expected to: + +- install and initialize the PostHog SDK first when the app doesn't have it (the flow's seed queues install/init like `wizard replay-vision` does) +- wire up exception capture through the SDK's own mechanism (autocapture at init, the framework's error handler, or an error boundary at the app entry) — in one place, never sprinkled manual capture calls +- on platforms that ship minified bundles or stripped binaries, wire source-map / debug-symbol upload into the production build, credentials and CI included +- write the run report to `./posthog-error-tracking-report.md` + +Each app's README lists only what is specific to it. diff --git a/apps/error-tracking/cicd-docker-node-raw/.dockerignore b/apps/error-tracking/cicd-docker-node-raw/.dockerignore new file mode 100644 index 000000000..bf43ca351 --- /dev/null +++ b/apps/error-tracking/cicd-docker-node-raw/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +*.log +.DS_Store +.env +.git +.github diff --git a/apps/error-tracking/cicd-docker-node-raw/.gitignore b/apps/error-tracking/cicd-docker-node-raw/.gitignore new file mode 100644 index 000000000..753521168 --- /dev/null +++ b/apps/error-tracking/cicd-docker-node-raw/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +*.log +.DS_Store diff --git a/apps/error-tracking/cicd-docker-node-raw/Dockerfile b/apps/error-tracking/cicd-docker-node-raw/Dockerfile new file mode 100644 index 000000000..3271a21ed --- /dev/null +++ b/apps/error-tracking/cicd-docker-node-raw/Dockerfile @@ -0,0 +1,30 @@ +# syntax=docker/dockerfile:1 + +# ---- Build stage ---- +# Compiles the TypeScript app into dist/. The image is built and run by hand +# (`docker build` / `docker run`) — this project has no CI pipeline. +FROM node:24-alpine AS build +WORKDIR /app + +# Install all dependencies (including dev deps such as typescript) using the lockfile. +COPY package.json package-lock.json ./ +RUN npm ci + +# Compile src/ -> dist/ +COPY tsconfig.json ./ +COPY src ./src +RUN npm run build + +# ---- Runtime stage ---- +# Ships only production dependencies and the compiled output. +FROM node:24-alpine AS runtime +ENV NODE_ENV=production +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev + +COPY --from=build /app/dist ./dist + +# POSTHOG_KEY / POSTHOG_HOST are provided at runtime (e.g. `docker run -e ...`). +CMD ["node", "dist/index.js"] diff --git a/apps/error-tracking/cicd-docker-node-raw/package-lock.json b/apps/error-tracking/cicd-docker-node-raw/package-lock.json new file mode 100644 index 000000000..889d2dc23 --- /dev/null +++ b/apps/error-tracking/cicd-docker-node-raw/package-lock.json @@ -0,0 +1,85 @@ +{ + "name": "cicd-docker-node-raw", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cicd-docker-node-raw", + "version": "0.1.0", + "dependencies": { + "posthog-node": "^5.35.4" + }, + "devDependencies": { + "@types/node": "^26.0.0", + "typescript": "^5.7.2" + } + }, + "node_modules/@posthog/core": { + "version": "1.29.13", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.29.13.tgz", + "integrity": "sha512-7Me5zaeAue/wmA364Go8ChYbsVAfNAHbtDxXopWu3D6hq9PVScUcauRgjD1njgvP8NzN91SrIllE+pri3XvJVw==", + "license": "MIT", + "dependencies": { + "@posthog/types": "1.376.4" + } + }, + "node_modules/@posthog/types": { + "version": "1.376.4", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.376.4.tgz", + "integrity": "sha512-EoDEvA925lf6yxPpbP4wozlXgu4b9WEqxZlFBUDd4k2akP5R/RWyHpvQT8aYyfY6BtSLn8TnVwxPQOM4b90isA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", + "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/posthog-node": { + "version": "5.35.6", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.35.6.tgz", + "integrity": "sha512-LwoXnR89A0l75jvFrXjtsAs0BbpyCjnwY8YIUkZT91rt1YnIUfBYiLj7qoUYApdNgesgWQQHqLXxLYX59a6ZYw==", + "license": "MIT", + "dependencies": { + "@posthog/core": "1.29.13" + }, + "engines": { + "node": "^20.20.0 || >=22.22.0" + }, + "peerDependencies": { + "rxjs": "^7.0.0" + }, + "peerDependenciesMeta": { + "rxjs": { + "optional": true + } + } + }, + "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/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/apps/error-tracking/cicd-docker-node-raw/package.json b/apps/error-tracking/cicd-docker-node-raw/package.json new file mode 100644 index 000000000..5f78903ff --- /dev/null +++ b/apps/error-tracking/cicd-docker-node-raw/package.json @@ -0,0 +1,17 @@ +{ + "name": "cicd-docker-node-raw", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc", + "start": "node --env-file=.env dist/index.js" + }, + "dependencies": { + "posthog-node": "^5.35.4" + }, + "devDependencies": { + "@types/node": "^26.0.0", + "typescript": "^5.7.2" + } +} diff --git a/apps/error-tracking/cicd-docker-node-raw/src/index.ts b/apps/error-tracking/cicd-docker-node-raw/src/index.ts new file mode 100644 index 000000000..006dfecf3 --- /dev/null +++ b/apps/error-tracking/cicd-docker-node-raw/src/index.ts @@ -0,0 +1,10 @@ +import { PostHog } from 'posthog-node' + +const client = new PostHog(process.env.POSTHOG_KEY ?? '', { + host: process.env.POSTHOG_HOST, +}) + +client.capture({ distinctId: 'node-raw', event: 'hello' }) + + +await client.shutdown() diff --git a/apps/error-tracking/cicd-docker-node-raw/tsconfig.json b/apps/error-tracking/cicd-docker-node-raw/tsconfig.json new file mode 100644 index 000000000..2659d22cd --- /dev/null +++ b/apps/error-tracking/cicd-docker-node-raw/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/apps/error-tracking/cicd-github-actions-docker-node-raw/.dockerignore b/apps/error-tracking/cicd-github-actions-docker-node-raw/.dockerignore new file mode 100644 index 000000000..bf43ca351 --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-docker-node-raw/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +*.log +.DS_Store +.env +.git +.github diff --git a/apps/error-tracking/cicd-github-actions-docker-node-raw/.github/workflows/deploy.yml b/apps/error-tracking/cicd-github-actions-docker-node-raw/.github/workflows/deploy.yml new file mode 100644 index 000000000..5fe79a47f --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-docker-node-raw/.github/workflows/deploy.yml @@ -0,0 +1,93 @@ +name: Deploy + +# Deploy pipeline: build a Docker image, push it to GHCR, then SSH into the +# target server and run the freshly built image. + +on: + push: + branches: + - main + workflow_dispatch: + +concurrency: + group: deploy-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + name: Build and push image + runs-on: ubuntu-latest + timeout-minutes: 20 + outputs: + image: ${{ steps.meta.outputs.tags }} + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=sha,format=long + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy: + name: Deploy to server over SSH + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: production + steps: + - name: SSH into server and run the new image + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + envs: REGISTRY,IMAGE_NAME,GITHUB_SHA + script: | + set -euo pipefail + IMAGE="${REGISTRY}/${IMAGE_NAME}:sha-${GITHUB_SHA}" + + echo "${{ secrets.GHCR_TOKEN }}" | docker login "${REGISTRY}" -u "${{ secrets.GHCR_USER }}" --password-stdin + docker pull "${IMAGE}" + + # Replace the running container with the freshly built image. + docker rm -f node-raw-app 2>/dev/null || true + docker run -d \ + --name node-raw-app \ + --restart unless-stopped \ + -e POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}" \ + -e POSTHOG_HOST="${{ secrets.POSTHOG_HOST }}" \ + "${IMAGE}" diff --git a/apps/error-tracking/cicd-github-actions-docker-node-raw/.gitignore b/apps/error-tracking/cicd-github-actions-docker-node-raw/.gitignore new file mode 100644 index 000000000..753521168 --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-docker-node-raw/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +*.log +.DS_Store diff --git a/apps/error-tracking/cicd-github-actions-docker-node-raw/Dockerfile b/apps/error-tracking/cicd-github-actions-docker-node-raw/Dockerfile new file mode 100644 index 000000000..6adcee8c0 --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-docker-node-raw/Dockerfile @@ -0,0 +1,29 @@ +# syntax=docker/dockerfile:1 + +# ---- Build stage ---- +# Compiles the TypeScript app into dist/. +FROM node:24-alpine AS build +WORKDIR /app + +# Install all dependencies (including dev deps such as typescript) using the lockfile. +COPY package.json package-lock.json ./ +RUN npm ci + +# Compile src/ -> dist/ +COPY tsconfig.json ./ +COPY src ./src +RUN npm run build + +# ---- Runtime stage ---- +# Ships only production dependencies and the compiled output. +FROM node:24-alpine AS runtime +ENV NODE_ENV=production +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev + +COPY --from=build /app/dist ./dist + +# POSTHOG_KEY / POSTHOG_HOST are provided at runtime (e.g. `docker run -e ...`). +CMD ["node", "dist/index.js"] diff --git a/apps/error-tracking/cicd-github-actions-docker-node-raw/package-lock.json b/apps/error-tracking/cicd-github-actions-docker-node-raw/package-lock.json new file mode 100644 index 000000000..aed4d76e3 --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-docker-node-raw/package-lock.json @@ -0,0 +1,85 @@ +{ + "name": "cicd-github-actions-docker-node-raw", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cicd-github-actions-docker-node-raw", + "version": "0.1.0", + "dependencies": { + "posthog-node": "^5.35.4" + }, + "devDependencies": { + "@types/node": "^26.0.0", + "typescript": "^5.7.2" + } + }, + "node_modules/@posthog/core": { + "version": "1.29.13", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.29.13.tgz", + "integrity": "sha512-7Me5zaeAue/wmA364Go8ChYbsVAfNAHbtDxXopWu3D6hq9PVScUcauRgjD1njgvP8NzN91SrIllE+pri3XvJVw==", + "license": "MIT", + "dependencies": { + "@posthog/types": "1.376.4" + } + }, + "node_modules/@posthog/types": { + "version": "1.376.4", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.376.4.tgz", + "integrity": "sha512-EoDEvA925lf6yxPpbP4wozlXgu4b9WEqxZlFBUDd4k2akP5R/RWyHpvQT8aYyfY6BtSLn8TnVwxPQOM4b90isA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", + "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/posthog-node": { + "version": "5.35.6", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.35.6.tgz", + "integrity": "sha512-LwoXnR89A0l75jvFrXjtsAs0BbpyCjnwY8YIUkZT91rt1YnIUfBYiLj7qoUYApdNgesgWQQHqLXxLYX59a6ZYw==", + "license": "MIT", + "dependencies": { + "@posthog/core": "1.29.13" + }, + "engines": { + "node": "^20.20.0 || >=22.22.0" + }, + "peerDependencies": { + "rxjs": "^7.0.0" + }, + "peerDependenciesMeta": { + "rxjs": { + "optional": true + } + } + }, + "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/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/apps/error-tracking/cicd-github-actions-docker-node-raw/package.json b/apps/error-tracking/cicd-github-actions-docker-node-raw/package.json new file mode 100644 index 000000000..8b47c8e68 --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-docker-node-raw/package.json @@ -0,0 +1,17 @@ +{ + "name": "cicd-github-actions-docker-node-raw", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc", + "start": "node --env-file=.env dist/index.js" + }, + "dependencies": { + "posthog-node": "^5.35.4" + }, + "devDependencies": { + "@types/node": "^26.0.0", + "typescript": "^5.7.2" + } +} diff --git a/apps/error-tracking/cicd-github-actions-docker-node-raw/src/index.ts b/apps/error-tracking/cicd-github-actions-docker-node-raw/src/index.ts new file mode 100644 index 000000000..006dfecf3 --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-docker-node-raw/src/index.ts @@ -0,0 +1,10 @@ +import { PostHog } from 'posthog-node' + +const client = new PostHog(process.env.POSTHOG_KEY ?? '', { + host: process.env.POSTHOG_HOST, +}) + +client.capture({ distinctId: 'node-raw', event: 'hello' }) + + +await client.shutdown() diff --git a/apps/error-tracking/cicd-github-actions-docker-node-raw/tsconfig.json b/apps/error-tracking/cicd-github-actions-docker-node-raw/tsconfig.json new file mode 100644 index 000000000..2659d22cd --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-docker-node-raw/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/.dockerignore b/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/.dockerignore new file mode 100644 index 000000000..bf43ca351 --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +*.log +.DS_Store +.env +.git +.github diff --git a/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/.github/actions/build-and-push/action.yml b/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/.github/actions/build-and-push/action.yml new file mode 100644 index 000000000..7a2c374d4 --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/.github/actions/build-and-push/action.yml @@ -0,0 +1,53 @@ +name: Build and push image +description: >- + Local composite action that builds the app's Docker image and pushes it to a + container registry. The deploy workflow invokes this with `uses:` and passes + inputs, instead of inlining the build steps. + +inputs: + registry: + description: Container registry host. + required: false + default: ghcr.io + image-name: + description: Image repository name, without the registry prefix. + required: true + registry-username: + description: Username used to authenticate against the registry. + required: true + registry-password: + description: Password or token used to authenticate against the registry. + required: true + +outputs: + image: + description: The fully-qualified image reference that was built and pushed. + value: ${{ steps.image.outputs.ref }} + +runs: + using: composite + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to registry + uses: docker/login-action@v3 + with: + registry: ${{ inputs.registry }} + username: ${{ inputs.registry-username }} + password: ${{ inputs.registry-password }} + + - name: Compute image reference + id: image + shell: bash + run: echo "ref=${{ inputs.registry }}/${{ inputs.image-name }}:${{ github.sha }}" >> "$GITHUB_OUTPUT" + + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.image.outputs.ref }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/.github/workflows/deploy.yml b/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/.github/workflows/deploy.yml new file mode 100644 index 000000000..e93652fea --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/.github/workflows/deploy.yml @@ -0,0 +1,72 @@ +name: Deploy + +# Deploy pipeline. The Docker build/push is not inlined here — it is delegated +# to a local composite action (.github/actions/build-and-push) that accepts +# inputs. + +on: + push: + branches: + - main + workflow_dispatch: + +concurrency: + group: deploy-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + packages: write + +jobs: + build: + name: Build and push image + runs-on: ubuntu-latest + timeout-minutes: 20 + outputs: + image: ${{ steps.build.outputs.image }} + steps: + - name: Checkout + uses: actions/checkout@v6 + + # Delegate the Docker build/push to the nested composite action, + # passing registry/image details as inputs. + - name: Build and push image + id: build + uses: ./.github/actions/build-and-push + with: + registry: ghcr.io + image-name: ${{ github.repository }} + registry-username: ${{ github.actor }} + registry-password: ${{ secrets.GITHUB_TOKEN }} + + deploy: + name: Deploy to server over SSH + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: production + steps: + - name: SSH into server and run the new image + uses: appleboy/ssh-action@v1 + env: + IMAGE: ${{ needs.build.outputs.image }} + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + envs: IMAGE + script: | + set -euo pipefail + + echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u "${{ secrets.GHCR_USER }}" --password-stdin + docker pull "${IMAGE}" + + # Replace the running container with the freshly built image. + docker rm -f node-raw-app 2>/dev/null || true + docker run -d \ + --name node-raw-app \ + --restart unless-stopped \ + -e POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}" \ + -e POSTHOG_HOST="${{ secrets.POSTHOG_HOST }}" \ + "${IMAGE}" diff --git a/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/.gitignore b/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/.gitignore new file mode 100644 index 000000000..753521168 --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +*.log +.DS_Store diff --git a/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/Dockerfile b/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/Dockerfile new file mode 100644 index 000000000..6adcee8c0 --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/Dockerfile @@ -0,0 +1,29 @@ +# syntax=docker/dockerfile:1 + +# ---- Build stage ---- +# Compiles the TypeScript app into dist/. +FROM node:24-alpine AS build +WORKDIR /app + +# Install all dependencies (including dev deps such as typescript) using the lockfile. +COPY package.json package-lock.json ./ +RUN npm ci + +# Compile src/ -> dist/ +COPY tsconfig.json ./ +COPY src ./src +RUN npm run build + +# ---- Runtime stage ---- +# Ships only production dependencies and the compiled output. +FROM node:24-alpine AS runtime +ENV NODE_ENV=production +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev + +COPY --from=build /app/dist ./dist + +# POSTHOG_KEY / POSTHOG_HOST are provided at runtime (e.g. `docker run -e ...`). +CMD ["node", "dist/index.js"] diff --git a/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/package-lock.json b/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/package-lock.json new file mode 100644 index 000000000..39917465b --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/package-lock.json @@ -0,0 +1,85 @@ +{ + "name": "cicd-github-actions-nested-docker-node-raw", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cicd-github-actions-nested-docker-node-raw", + "version": "0.1.0", + "dependencies": { + "posthog-node": "^5.35.4" + }, + "devDependencies": { + "@types/node": "^26.0.0", + "typescript": "^5.7.2" + } + }, + "node_modules/@posthog/core": { + "version": "1.29.13", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.29.13.tgz", + "integrity": "sha512-7Me5zaeAue/wmA364Go8ChYbsVAfNAHbtDxXopWu3D6hq9PVScUcauRgjD1njgvP8NzN91SrIllE+pri3XvJVw==", + "license": "MIT", + "dependencies": { + "@posthog/types": "1.376.4" + } + }, + "node_modules/@posthog/types": { + "version": "1.376.4", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.376.4.tgz", + "integrity": "sha512-EoDEvA925lf6yxPpbP4wozlXgu4b9WEqxZlFBUDd4k2akP5R/RWyHpvQT8aYyfY6BtSLn8TnVwxPQOM4b90isA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", + "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/posthog-node": { + "version": "5.35.6", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.35.6.tgz", + "integrity": "sha512-LwoXnR89A0l75jvFrXjtsAs0BbpyCjnwY8YIUkZT91rt1YnIUfBYiLj7qoUYApdNgesgWQQHqLXxLYX59a6ZYw==", + "license": "MIT", + "dependencies": { + "@posthog/core": "1.29.13" + }, + "engines": { + "node": "^20.20.0 || >=22.22.0" + }, + "peerDependencies": { + "rxjs": "^7.0.0" + }, + "peerDependenciesMeta": { + "rxjs": { + "optional": true + } + } + }, + "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/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/package.json b/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/package.json new file mode 100644 index 000000000..e90859be0 --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/package.json @@ -0,0 +1,17 @@ +{ + "name": "cicd-github-actions-nested-docker-node-raw", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc", + "start": "node --env-file=.env dist/index.js" + }, + "dependencies": { + "posthog-node": "^5.35.4" + }, + "devDependencies": { + "@types/node": "^26.0.0", + "typescript": "^5.7.2" + } +} diff --git a/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/src/index.ts b/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/src/index.ts new file mode 100644 index 000000000..006dfecf3 --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/src/index.ts @@ -0,0 +1,10 @@ +import { PostHog } from 'posthog-node' + +const client = new PostHog(process.env.POSTHOG_KEY ?? '', { + host: process.env.POSTHOG_HOST, +}) + +client.capture({ distinctId: 'node-raw', event: 'hello' }) + + +await client.shutdown() diff --git a/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/tsconfig.json b/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/tsconfig.json new file mode 100644 index 000000000..2659d22cd --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-nested-docker-node-raw/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/apps/error-tracking/cicd-github-actions-node-raw/.github/workflows/deploy.yml b/apps/error-tracking/cicd-github-actions-node-raw/.github/workflows/deploy.yml new file mode 100644 index 000000000..3487ad9da --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-node-raw/.github/workflows/deploy.yml @@ -0,0 +1,63 @@ +name: Deploy + +# Deploy pipeline: no Docker. The app is built directly on the GitHub Actions +# runner, the built output is copied to the server, and the app is restarted +# over SSH — the server itself never builds anything. + +on: + push: + branches: + - main + workflow_dispatch: + +concurrency: + group: deploy-${{ github.ref }} + cancel-in-progress: true + +jobs: + deploy: + name: Build on the runner, deploy over SSH + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: production + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Copy the built app to the server + uses: appleboy/scp-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + source: 'dist/*,package.json,package-lock.json' + target: /srv/cicd-github-actions-node-raw + + - name: Restart the app on the server + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + script: | + set -euo pipefail + cd /srv/cicd-github-actions-node-raw + npm ci --omit=dev + + # Replace the running instance with the freshly copied build. + pkill -f "node dist/index.js" || true + POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}" \ + POSTHOG_HOST="${{ secrets.POSTHOG_HOST }}" \ + nohup node dist/index.js > app.log 2>&1 & diff --git a/apps/error-tracking/cicd-github-actions-node-raw/.gitignore b/apps/error-tracking/cicd-github-actions-node-raw/.gitignore new file mode 100644 index 000000000..753521168 --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-node-raw/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +*.log +.DS_Store diff --git a/apps/error-tracking/cicd-github-actions-node-raw/package-lock.json b/apps/error-tracking/cicd-github-actions-node-raw/package-lock.json new file mode 100644 index 000000000..752e4c309 --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-node-raw/package-lock.json @@ -0,0 +1,85 @@ +{ + "name": "cicd-github-actions-node-raw", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cicd-github-actions-node-raw", + "version": "0.1.0", + "dependencies": { + "posthog-node": "^5.35.4" + }, + "devDependencies": { + "@types/node": "^26.0.0", + "typescript": "^5.7.2" + } + }, + "node_modules/@posthog/core": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.31.1.tgz", + "integrity": "sha512-ZV7qk1lSLpJR7n5HZ/au0iA1H/5TaZjwAF3pVVhQYwCALOSIF5jvnI2X9mvByQIS+wSUckPkZQ+33YaIT6SZJQ==", + "license": "MIT", + "dependencies": { + "@posthog/types": "1.384.1" + } + }, + "node_modules/@posthog/types": { + "version": "1.384.1", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.384.1.tgz", + "integrity": "sha512-tzObc/AIunXrKGX3lsPGQ1vs915L0KEChBDmPvgs9PMkCc7sH9eMn8/XKPbZ/BxJALRN8fsWCeSkR2fyoADWvA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", + "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/posthog-node": { + "version": "5.36.10", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.36.10.tgz", + "integrity": "sha512-1be0En8VC/GTzGhf8lrIte7W5T4tBOkCYYQTl2FQiAGpIx0kmVRusby3AHXD/dZP44jLN1aMet/a4AvMDjO33g==", + "license": "MIT", + "dependencies": { + "@posthog/core": "1.31.1" + }, + "engines": { + "node": "^20.20.0 || >=22.22.0" + }, + "peerDependencies": { + "rxjs": "^7.0.0" + }, + "peerDependenciesMeta": { + "rxjs": { + "optional": true + } + } + }, + "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/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/apps/error-tracking/cicd-github-actions-node-raw/package.json b/apps/error-tracking/cicd-github-actions-node-raw/package.json new file mode 100644 index 000000000..fe0ec8da4 --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-node-raw/package.json @@ -0,0 +1,17 @@ +{ + "name": "cicd-github-actions-node-raw", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc", + "start": "node --env-file=.env dist/index.js" + }, + "dependencies": { + "posthog-node": "^5.35.4" + }, + "devDependencies": { + "@types/node": "^26.0.0", + "typescript": "^5.7.2" + } +} diff --git a/apps/error-tracking/cicd-github-actions-node-raw/src/index.ts b/apps/error-tracking/cicd-github-actions-node-raw/src/index.ts new file mode 100644 index 000000000..006dfecf3 --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-node-raw/src/index.ts @@ -0,0 +1,10 @@ +import { PostHog } from 'posthog-node' + +const client = new PostHog(process.env.POSTHOG_KEY ?? '', { + host: process.env.POSTHOG_HOST, +}) + +client.capture({ distinctId: 'node-raw', event: 'hello' }) + + +await client.shutdown() diff --git a/apps/error-tracking/cicd-github-actions-node-raw/tsconfig.json b/apps/error-tracking/cicd-github-actions-node-raw/tsconfig.json new file mode 100644 index 000000000..2659d22cd --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-node-raw/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/apps/error-tracking/cicd-github-actions-single-stage-docker-node-raw/.dockerignore b/apps/error-tracking/cicd-github-actions-single-stage-docker-node-raw/.dockerignore new file mode 100644 index 000000000..bf43ca351 --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-single-stage-docker-node-raw/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +*.log +.DS_Store +.env +.git +.github diff --git a/apps/error-tracking/cicd-github-actions-single-stage-docker-node-raw/.github/workflows/deploy.yml b/apps/error-tracking/cicd-github-actions-single-stage-docker-node-raw/.github/workflows/deploy.yml new file mode 100644 index 000000000..5fe79a47f --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-single-stage-docker-node-raw/.github/workflows/deploy.yml @@ -0,0 +1,93 @@ +name: Deploy + +# Deploy pipeline: build a Docker image, push it to GHCR, then SSH into the +# target server and run the freshly built image. + +on: + push: + branches: + - main + workflow_dispatch: + +concurrency: + group: deploy-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + name: Build and push image + runs-on: ubuntu-latest + timeout-minutes: 20 + outputs: + image: ${{ steps.meta.outputs.tags }} + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=sha,format=long + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy: + name: Deploy to server over SSH + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: production + steps: + - name: SSH into server and run the new image + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + envs: REGISTRY,IMAGE_NAME,GITHUB_SHA + script: | + set -euo pipefail + IMAGE="${REGISTRY}/${IMAGE_NAME}:sha-${GITHUB_SHA}" + + echo "${{ secrets.GHCR_TOKEN }}" | docker login "${REGISTRY}" -u "${{ secrets.GHCR_USER }}" --password-stdin + docker pull "${IMAGE}" + + # Replace the running container with the freshly built image. + docker rm -f node-raw-app 2>/dev/null || true + docker run -d \ + --name node-raw-app \ + --restart unless-stopped \ + -e POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}" \ + -e POSTHOG_HOST="${{ secrets.POSTHOG_HOST }}" \ + "${IMAGE}" diff --git a/apps/error-tracking/cicd-github-actions-single-stage-docker-node-raw/.gitignore b/apps/error-tracking/cicd-github-actions-single-stage-docker-node-raw/.gitignore new file mode 100644 index 000000000..753521168 --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-single-stage-docker-node-raw/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +*.log +.DS_Store diff --git a/apps/error-tracking/cicd-github-actions-single-stage-docker-node-raw/Dockerfile b/apps/error-tracking/cicd-github-actions-single-stage-docker-node-raw/Dockerfile new file mode 100644 index 000000000..e9b82a697 --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-single-stage-docker-node-raw/Dockerfile @@ -0,0 +1,21 @@ +# syntax=docker/dockerfile:1 + +# Single-stage build: install dependencies, compile TypeScript, and run the app +# all from one image. Simpler than a multi-stage build, at the cost of a larger +# final image (dev dependencies and source files stay in the layers). +FROM node:24-alpine +WORKDIR /app + +# Install all dependencies (including dev deps such as typescript) using the lockfile. +COPY package.json package-lock.json ./ +RUN npm ci + +# Compile src/ -> dist/ +COPY tsconfig.json ./ +COPY src ./src +RUN npm run build + +ENV NODE_ENV=production + +# POSTHOG_KEY / POSTHOG_HOST are provided at runtime (e.g. `docker run -e ...`). +CMD ["node", "dist/index.js"] diff --git a/apps/error-tracking/cicd-github-actions-single-stage-docker-node-raw/package-lock.json b/apps/error-tracking/cicd-github-actions-single-stage-docker-node-raw/package-lock.json new file mode 100644 index 000000000..d8d0f9eac --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-single-stage-docker-node-raw/package-lock.json @@ -0,0 +1,85 @@ +{ + "name": "cicd-github-actions-single-stage-docker-node-raw", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cicd-github-actions-single-stage-docker-node-raw", + "version": "0.1.0", + "dependencies": { + "posthog-node": "^5.35.4" + }, + "devDependencies": { + "@types/node": "^26.0.0", + "typescript": "^5.7.2" + } + }, + "node_modules/@posthog/core": { + "version": "1.29.13", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.29.13.tgz", + "integrity": "sha512-7Me5zaeAue/wmA364Go8ChYbsVAfNAHbtDxXopWu3D6hq9PVScUcauRgjD1njgvP8NzN91SrIllE+pri3XvJVw==", + "license": "MIT", + "dependencies": { + "@posthog/types": "1.376.4" + } + }, + "node_modules/@posthog/types": { + "version": "1.376.4", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.376.4.tgz", + "integrity": "sha512-EoDEvA925lf6yxPpbP4wozlXgu4b9WEqxZlFBUDd4k2akP5R/RWyHpvQT8aYyfY6BtSLn8TnVwxPQOM4b90isA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", + "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/posthog-node": { + "version": "5.35.6", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.35.6.tgz", + "integrity": "sha512-LwoXnR89A0l75jvFrXjtsAs0BbpyCjnwY8YIUkZT91rt1YnIUfBYiLj7qoUYApdNgesgWQQHqLXxLYX59a6ZYw==", + "license": "MIT", + "dependencies": { + "@posthog/core": "1.29.13" + }, + "engines": { + "node": "^20.20.0 || >=22.22.0" + }, + "peerDependencies": { + "rxjs": "^7.0.0" + }, + "peerDependenciesMeta": { + "rxjs": { + "optional": true + } + } + }, + "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/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/apps/error-tracking/cicd-github-actions-single-stage-docker-node-raw/package.json b/apps/error-tracking/cicd-github-actions-single-stage-docker-node-raw/package.json new file mode 100644 index 000000000..2339203ac --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-single-stage-docker-node-raw/package.json @@ -0,0 +1,17 @@ +{ + "name": "cicd-github-actions-single-stage-docker-node-raw", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc", + "start": "node --env-file=.env dist/index.js" + }, + "dependencies": { + "posthog-node": "^5.35.4" + }, + "devDependencies": { + "@types/node": "^26.0.0", + "typescript": "^5.7.2" + } +} diff --git a/apps/error-tracking/cicd-github-actions-single-stage-docker-node-raw/src/index.ts b/apps/error-tracking/cicd-github-actions-single-stage-docker-node-raw/src/index.ts new file mode 100644 index 000000000..006dfecf3 --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-single-stage-docker-node-raw/src/index.ts @@ -0,0 +1,10 @@ +import { PostHog } from 'posthog-node' + +const client = new PostHog(process.env.POSTHOG_KEY ?? '', { + host: process.env.POSTHOG_HOST, +}) + +client.capture({ distinctId: 'node-raw', event: 'hello' }) + + +await client.shutdown() diff --git a/apps/error-tracking/cicd-github-actions-single-stage-docker-node-raw/tsconfig.json b/apps/error-tracking/cicd-github-actions-single-stage-docker-node-raw/tsconfig.json new file mode 100644 index 000000000..2659d22cd --- /dev/null +++ b/apps/error-tracking/cicd-github-actions-single-stage-docker-node-raw/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/apps/error-tracking/cicd-gitlab-node-raw/.gitignore b/apps/error-tracking/cicd-gitlab-node-raw/.gitignore new file mode 100644 index 000000000..753521168 --- /dev/null +++ b/apps/error-tracking/cicd-gitlab-node-raw/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +*.log +.DS_Store diff --git a/apps/error-tracking/cicd-gitlab-node-raw/.gitlab-ci.yml b/apps/error-tracking/cicd-gitlab-node-raw/.gitlab-ci.yml new file mode 100644 index 000000000..ae69a5008 --- /dev/null +++ b/apps/error-tracking/cicd-gitlab-node-raw/.gitlab-ci.yml @@ -0,0 +1,31 @@ +# Deploy pipeline for GitLab CI: build the app in the build job, hand the +# compiled output to the deploy job as an artifact, then copy it to the server +# and restart the app over SSH. + +stages: + - build + - deploy + +build: + stage: build + image: node:22-slim + script: + - npm ci + - npm run build + artifacts: + paths: + - dist/ + +deploy: + stage: deploy + image: node:22-slim + needs: + - build + environment: production + before_script: + - apt-get update -qq && apt-get install -y -qq openssh-client rsync + script: + - rsync -avz --delete dist/ package.json package-lock.json "$DEPLOY_USER@$DEPLOY_HOST:/srv/cicd-gitlab-node-raw/" + - ssh "$DEPLOY_USER@$DEPLOY_HOST" "cd /srv/cicd-gitlab-node-raw && npm ci --omit=dev && (pkill -f 'node dist/index.js' || true) && POSTHOG_KEY='$POSTHOG_KEY' POSTHOG_HOST='$POSTHOG_HOST' nohup node dist/index.js > app.log 2>&1 &" + rules: + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH diff --git a/apps/error-tracking/cicd-gitlab-node-raw/package-lock.json b/apps/error-tracking/cicd-gitlab-node-raw/package-lock.json new file mode 100644 index 000000000..71ab897ae --- /dev/null +++ b/apps/error-tracking/cicd-gitlab-node-raw/package-lock.json @@ -0,0 +1,85 @@ +{ + "name": "cicd-gitlab-node-raw", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cicd-gitlab-node-raw", + "version": "0.1.0", + "dependencies": { + "posthog-node": "^5.35.4" + }, + "devDependencies": { + "@types/node": "^26.0.0", + "typescript": "^5.7.2" + } + }, + "node_modules/@posthog/core": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.31.1.tgz", + "integrity": "sha512-ZV7qk1lSLpJR7n5HZ/au0iA1H/5TaZjwAF3pVVhQYwCALOSIF5jvnI2X9mvByQIS+wSUckPkZQ+33YaIT6SZJQ==", + "license": "MIT", + "dependencies": { + "@posthog/types": "1.384.1" + } + }, + "node_modules/@posthog/types": { + "version": "1.384.1", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.384.1.tgz", + "integrity": "sha512-tzObc/AIunXrKGX3lsPGQ1vs915L0KEChBDmPvgs9PMkCc7sH9eMn8/XKPbZ/BxJALRN8fsWCeSkR2fyoADWvA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", + "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/posthog-node": { + "version": "5.36.10", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.36.10.tgz", + "integrity": "sha512-1be0En8VC/GTzGhf8lrIte7W5T4tBOkCYYQTl2FQiAGpIx0kmVRusby3AHXD/dZP44jLN1aMet/a4AvMDjO33g==", + "license": "MIT", + "dependencies": { + "@posthog/core": "1.31.1" + }, + "engines": { + "node": "^20.20.0 || >=22.22.0" + }, + "peerDependencies": { + "rxjs": "^7.0.0" + }, + "peerDependenciesMeta": { + "rxjs": { + "optional": true + } + } + }, + "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/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/apps/error-tracking/cicd-gitlab-node-raw/package.json b/apps/error-tracking/cicd-gitlab-node-raw/package.json new file mode 100644 index 000000000..720ad3317 --- /dev/null +++ b/apps/error-tracking/cicd-gitlab-node-raw/package.json @@ -0,0 +1,17 @@ +{ + "name": "cicd-gitlab-node-raw", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc", + "start": "node --env-file=.env dist/index.js" + }, + "dependencies": { + "posthog-node": "^5.35.4" + }, + "devDependencies": { + "@types/node": "^26.0.0", + "typescript": "^5.7.2" + } +} diff --git a/apps/error-tracking/cicd-gitlab-node-raw/src/index.ts b/apps/error-tracking/cicd-gitlab-node-raw/src/index.ts new file mode 100644 index 000000000..006dfecf3 --- /dev/null +++ b/apps/error-tracking/cicd-gitlab-node-raw/src/index.ts @@ -0,0 +1,10 @@ +import { PostHog } from 'posthog-node' + +const client = new PostHog(process.env.POSTHOG_KEY ?? '', { + host: process.env.POSTHOG_HOST, +}) + +client.capture({ distinctId: 'node-raw', event: 'hello' }) + + +await client.shutdown() diff --git a/apps/error-tracking/cicd-gitlab-node-raw/tsconfig.json b/apps/error-tracking/cicd-gitlab-node-raw/tsconfig.json new file mode 100644 index 000000000..2659d22cd --- /dev/null +++ b/apps/error-tracking/cicd-gitlab-node-raw/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/apps/error-tracking/cicd-monorepo-pnpm-node-react/.dockerignore b/apps/error-tracking/cicd-monorepo-pnpm-node-react/.dockerignore new file mode 100644 index 000000000..856ad3d39 --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-pnpm-node-react/.dockerignore @@ -0,0 +1,9 @@ +**/node_modules +**/dist +**/*.log +**/.env +.DS_Store +.git +.github +frontend +.pnpm-store diff --git a/apps/error-tracking/cicd-monorepo-pnpm-node-react/.github/workflows/deploy-backend.yml b/apps/error-tracking/cicd-monorepo-pnpm-node-react/.github/workflows/deploy-backend.yml new file mode 100644 index 000000000..95822df1f --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-pnpm-node-react/.github/workflows/deploy-backend.yml @@ -0,0 +1,101 @@ +name: Deploy backend + +# Deploy pipeline for the backend only: build a Docker image from the pnpm +# workspace root (so the shared package is available), push it to GHCR, then SSH +# into the target server and run the freshly built image. The React/Vite +# frontend is intentionally left out of this workflow. + +on: + push: + branches: + - main + paths: + - 'backend/**' + - 'packages/shared/**' + - 'pnpm-workspace.yaml' + - 'pnpm-lock.yaml' + - '.github/workflows/deploy-backend.yml' + workflow_dispatch: + +concurrency: + group: deploy-backend-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + name: Build and push image + runs-on: ubuntu-latest + timeout-minutes: 20 + outputs: + image: ${{ steps.meta.outputs.tags }} + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=sha,format=long + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: backend/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy: + name: Deploy to server over SSH + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: production + steps: + - name: SSH into server and run the new image + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + envs: REGISTRY,IMAGE_NAME,GITHUB_SHA + script: | + set -euo pipefail + IMAGE="${REGISTRY}/${IMAGE_NAME}:sha-${GITHUB_SHA}" + + echo "${{ secrets.GHCR_TOKEN }}" | docker login "${REGISTRY}" -u "${{ secrets.GHCR_USER }}" --password-stdin + docker pull "${IMAGE}" + + # Replace the running container with the freshly built image. + docker rm -f backend-app 2>/dev/null || true + docker run -d \ + --name backend-app \ + --restart unless-stopped \ + -e POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}" \ + -e POSTHOG_HOST="${{ secrets.POSTHOG_HOST }}" \ + "${IMAGE}" diff --git a/apps/error-tracking/cicd-monorepo-pnpm-node-react/.gitignore b/apps/error-tracking/cicd-monorepo-pnpm-node-react/.gitignore new file mode 100644 index 000000000..2a1a3fa07 --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-pnpm-node-react/.gitignore @@ -0,0 +1,7 @@ +node_modules +dist +dist-ssr +*.log +.DS_Store +*.local +.pnpm-store diff --git a/apps/error-tracking/cicd-monorepo-pnpm-node-react/backend/Dockerfile b/apps/error-tracking/cicd-monorepo-pnpm-node-react/backend/Dockerfile new file mode 100644 index 000000000..7b6d8dae5 --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-pnpm-node-react/backend/Dockerfile @@ -0,0 +1,52 @@ +# syntax=docker/dockerfile:1 + +# Builds the backend (and its workspace dependencies) from the pnpm monorepo +# root. The build context is the repository root so that pnpm-workspace.yaml, +# the lockfile and the shared package are all available. The React/Vite +# frontend is intentionally excluded (see .dockerignore). + +# ---- Build stage ---- +FROM node:24-alpine AS build +ENV PNPM_HOME=/pnpm +ENV PATH=$PNPM_HOME:$PATH +RUN corepack enable +WORKDIR /repo + +# Install only what the `backend` workspace needs (plus its workspace deps) +# using the frozen lockfile. Copying just the manifests first keeps this layer +# cached until a package.json actually changes. +COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./ +COPY packages/shared/package.json packages/shared/ +COPY backend/package.json backend/ +RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ + pnpm install --frozen-lockfile --filter backend... + +# Compile the sources. Shared is built before the backend so its dist/ + types +# are in place before the backend is compiled. +COPY packages/shared packages/shared +COPY backend backend +RUN pnpm --filter @repo/shared build && pnpm --filter backend build + +# ---- Runtime stage ---- +# Reinstall production-only dependencies and copy in the compiled output. The +# prod install recreates the backend -> @repo/shared workspace symlink, so we +# just drop the built dist/ folders into place. +FROM node:24-alpine AS runtime +ENV NODE_ENV=production +ENV PNPM_HOME=/pnpm +ENV PATH=$PNPM_HOME:$PATH +RUN corepack enable +WORKDIR /repo + +COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./ +COPY packages/shared/package.json packages/shared/ +COPY backend/package.json backend/ +RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ + pnpm install --frozen-lockfile --prod --filter backend... + +COPY --from=build /repo/packages/shared/dist packages/shared/dist +COPY --from=build /repo/backend/dist backend/dist + +WORKDIR /repo/backend +# POSTHOG_KEY / POSTHOG_HOST are provided at runtime (e.g. `docker run -e ...`). +CMD ["node", "dist/index.js"] diff --git a/apps/error-tracking/cicd-monorepo-pnpm-node-react/backend/package.json b/apps/error-tracking/cicd-monorepo-pnpm-node-react/backend/package.json new file mode 100644 index 000000000..92156705a --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-pnpm-node-react/backend/package.json @@ -0,0 +1,18 @@ +{ + "name": "backend", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc", + "start": "node --env-file=.env dist/index.js" + }, + "dependencies": { + "@repo/shared": "workspace:*", + "posthog-node": "^5.35.4" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "typescript": "^5.7.2" + } +} diff --git a/apps/error-tracking/cicd-monorepo-pnpm-node-react/backend/src/index.ts b/apps/error-tracking/cicd-monorepo-pnpm-node-react/backend/src/index.ts new file mode 100644 index 000000000..5616a5027 --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-pnpm-node-react/backend/src/index.ts @@ -0,0 +1,11 @@ +import { PostHog } from 'posthog-node' +import { eventName } from '@repo/shared' + +const client = new PostHog(process.env.POSTHOG_KEY ?? '', { + host: process.env.POSTHOG_HOST, +}) + +client.capture({ distinctId: 'backend', event: eventName('backend', 'hello') }) + + +await client.shutdown() diff --git a/apps/error-tracking/cicd-monorepo-pnpm-node-react/backend/tsconfig.json b/apps/error-tracking/cicd-monorepo-pnpm-node-react/backend/tsconfig.json new file mode 100644 index 000000000..2659d22cd --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-pnpm-node-react/backend/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/index.html b/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/index.html new file mode 100644 index 000000000..0fca6f043 --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + frontend + + +
+ + + diff --git a/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/package.json b/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/package.json new file mode 100644 index 000000000..ff7767c82 --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/package.json @@ -0,0 +1,24 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@repo/shared": "workspace:*", + "posthog-js": "^1.376.2", + "react": "^19.2.6", + "react-dom": "^19.2.6" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "typescript": "~6.0.2", + "vite": "^8.0.12" + } +} diff --git a/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/public/favicon.svg b/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/public/favicon.svg new file mode 100644 index 000000000..6893eb132 --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/src/App.tsx b/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/src/App.tsx new file mode 100644 index 000000000..e0f833691 --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/src/App.tsx @@ -0,0 +1,18 @@ +import { usePostHog } from 'posthog-js/react' +import { eventName } from '@repo/shared' + +function App() { + const posthog = usePostHog() + + return ( +
+

Vite + React + PostHog test fixture (pnpm workspace)

+

This page exists to give the bundler something to ship.

+ +
+ ) +} + +export default App diff --git a/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/src/main.tsx b/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/src/main.tsx new file mode 100644 index 000000000..d302474cd --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/src/main.tsx @@ -0,0 +1,19 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { PostHogProvider } from 'posthog-js/react' +import App from './App.tsx' + +const options = { + api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST, +} + +createRoot(document.getElementById('root')!).render( + + + + + , +) diff --git a/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/tsconfig.app.json b/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/tsconfig.app.json new file mode 100644 index 000000000..7f42e5f7c --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/tsconfig.app.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/tsconfig.json b/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/tsconfig.json new file mode 100644 index 000000000..1ffef600d --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/tsconfig.node.json b/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/tsconfig.node.json new file mode 100644 index 000000000..9794fb4f5 --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "module": "esnext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/vite.config.ts b/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/vite.config.ts new file mode 100644 index 000000000..8b0f57b91 --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-pnpm-node-react/frontend/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/apps/error-tracking/cicd-monorepo-pnpm-node-react/package.json b/apps/error-tracking/cicd-monorepo-pnpm-node-react/package.json new file mode 100644 index 000000000..c3028506b --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-pnpm-node-react/package.json @@ -0,0 +1,9 @@ +{ + "name": "cicd-monorepo-pnpm-node-react", + "version": "0.0.0", + "private": true, + "scripts": { + "build": "pnpm -r build" + }, + "packageManager": "pnpm@10.23.0" +} diff --git a/apps/error-tracking/cicd-monorepo-pnpm-node-react/packages/shared/package.json b/apps/error-tracking/cicd-monorepo-pnpm-node-react/packages/shared/package.json new file mode 100644 index 000000000..392167d2c --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-pnpm-node-react/packages/shared/package.json @@ -0,0 +1,23 @@ +{ + "name": "@repo/shared", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc" + }, + "devDependencies": { + "typescript": "^5.7.2" + } +} diff --git a/apps/error-tracking/cicd-monorepo-pnpm-node-react/packages/shared/src/index.ts b/apps/error-tracking/cicd-monorepo-pnpm-node-react/packages/shared/src/index.ts new file mode 100644 index 000000000..dd15d7e30 --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-pnpm-node-react/packages/shared/src/index.ts @@ -0,0 +1,12 @@ +export const APP_NAME = 'cicd-monorepo-pnpm-node-react' + +export type AnalyticsSource = 'backend' | 'frontend' + +/** + * Builds a namespaced event name shared across the workspace so that backend + * and frontend events line up under the same convention in PostHog. Lives in + * the shared package so both apps share the same event-naming logic. + */ +export function eventName(source: AnalyticsSource, name: string): string { + return `${APP_NAME}:${source}:${name}` +} diff --git a/apps/error-tracking/cicd-monorepo-pnpm-node-react/packages/shared/tsconfig.json b/apps/error-tracking/cicd-monorepo-pnpm-node-react/packages/shared/tsconfig.json new file mode 100644 index 000000000..cf00e9852 --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-pnpm-node-react/packages/shared/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "strict": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/apps/error-tracking/cicd-monorepo-pnpm-node-react/pnpm-lock.yaml b/apps/error-tracking/cicd-monorepo-pnpm-node-react/pnpm-lock.yaml new file mode 100644 index 000000000..d8127433c --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-pnpm-node-react/pnpm-lock.yaml @@ -0,0 +1,698 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: {} + + backend: + dependencies: + '@repo/shared': + specifier: workspace:* + version: link:../packages/shared + posthog-node: + specifier: ^5.35.4 + version: 5.38.2 + devDependencies: + '@types/node': + specifier: ^22.10.2 + version: 22.19.21 + typescript: + specifier: ^5.7.2 + version: 5.9.3 + + frontend: + dependencies: + '@repo/shared': + specifier: workspace:* + version: link:../packages/shared + posthog-js: + specifier: ^1.376.2 + version: 1.391.2 + react: + specifier: ^19.2.6 + version: 19.2.7 + react-dom: + specifier: ^19.2.6 + version: 19.2.7(react@19.2.7) + devDependencies: + '@types/react': + specifier: ^19.2.14 + version: 19.2.17 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.17) + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.2(vite@8.0.16(@types/node@22.19.21)) + typescript: + specifier: ~6.0.2 + version: 6.0.3 + vite: + specifier: ^8.0.12 + version: 8.0.16(@types/node@22.19.21) + + packages/shared: + devDependencies: + typescript: + specifier: ^5.7.2 + version: 5.9.3 + +packages: + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@napi-rs/wasm-runtime@1.1.5': + resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.133.0': + resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + + '@posthog/core@1.35.3': + resolution: {integrity: sha512-EsGPbSLl39Jgo2KZ+kI9UAxFnh5nddaN5bNm2rXvUwF+vGmam9eN1EXeNbxhRU7ulEeIiGdm7XjoU7pzavkgIQ==} + + '@posthog/types@1.390.2': + resolution: {integrity: sha512-WcfKz2GNn2vfDX8vXmJYbKxegPxVWHuDQ/pHdAn0HoZDXDFnEp/+x3qBQA+fEvtbPjjtjgAt2wIgJMlM7asx7g==} + + '@rolldown/binding-android-arm64@1.0.3': + resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.3': + resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.3': + resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.3': + resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.3': + resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.3': + resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.3': + resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.3': + resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.3': + resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.3': + resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.3': + resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.3': + resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.3': + resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + + '@types/node@22.19.21': + resolution: {integrity: sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@vitejs/plugin-react@6.0.2': + resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + core-js@3.49.0: + resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dompurify@3.4.11: + resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.4.8: + resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + nanoid@3.3.13: + resolution: {integrity: sha512-sPdqC6ByMVVGvF1ynvvMo0/o+oD1VX7DaHhijt1bFgjvBkHBib4t49GoNDhf2NDta4oeUNlaGbSt5K7qjZ955Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + posthog-js@1.391.2: + resolution: {integrity: sha512-q0DZN6ljchSnAFJIXf+sQFTPlsLjTlRa+TvrL+QRb6413BGtib/MNiQy1bnwLKt8KR+f6xJYvkqdLyty9s4Aww==} + + posthog-node@5.38.2: + resolution: {integrity: sha512-eiKpU+vX4hVuHbO/EosvPHsmh2AVIdoVmWss/uUOs1t4b0ViCblw2o8OIFqHxKj3mYRnSOBlX0Dw3wBvcCaYpA==} + engines: {node: ^20.20.0 || >=22.22.0} + peerDependencies: + rxjs: ^7.0.0 + peerDependenciesMeta: + rxjs: + optional: true + + preact@10.29.2: + resolution: {integrity: sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==} + + query-selector-shadow-dom@1.0.1: + resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} + + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + peerDependencies: + react: ^19.2.7 + + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + engines: {node: '>=0.10.0'} + + rolldown@1.0.3: + resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + vite@8.0.16: + resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + web-vitals@5.3.0: + resolution: {integrity: sha512-q6LWsLatGYZp5VGBIOvbTj6JBV2nOmC8KvWztXBmwJcfFAzhwKwbOxhUH306XY3CcaZDUlSmSuNPBsCn0bFu+g==} + +snapshots: + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@oxc-project/types@0.133.0': {} + + '@posthog/core@1.35.3': + dependencies: + '@posthog/types': 1.390.2 + + '@posthog/types@1.390.2': {} + + '@rolldown/binding-android-arm64@1.0.3': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.3': + optional: true + + '@rolldown/binding-darwin-x64@1.0.3': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.3': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.3': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.3': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.3': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.3': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.3': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.3': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/node@22.19.21': + dependencies: + undici-types: 6.21.0 + + '@types/react-dom@19.2.3(@types/react@19.2.17)': + dependencies: + '@types/react': 19.2.17 + + '@types/react@19.2.17': + dependencies: + csstype: 3.2.3 + + '@types/trusted-types@2.0.7': + optional: true + + '@vitejs/plugin-react@6.0.2(vite@8.0.16(@types/node@22.19.21))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.0.16(@types/node@22.19.21) + + core-js@3.49.0: {} + + csstype@3.2.3: {} + + detect-libc@2.1.2: {} + + dompurify@3.4.11: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fflate@0.4.8: {} + + fsevents@2.3.3: + optional: true + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + nanoid@3.3.13: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.13 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + posthog-js@1.391.2: + dependencies: + '@posthog/core': 1.35.3 + '@posthog/types': 1.390.2 + core-js: 3.49.0 + dompurify: 3.4.11 + fflate: 0.4.8 + preact: 10.29.2 + query-selector-shadow-dom: 1.0.1 + web-vitals: 5.3.0 + + posthog-node@5.38.2: + dependencies: + '@posthog/core': 1.35.3 + + preact@10.29.2: {} + + query-selector-shadow-dom@1.0.1: {} + + react-dom@19.2.7(react@19.2.7): + dependencies: + react: 19.2.7 + scheduler: 0.27.0 + + react@19.2.7: {} + + rolldown@1.0.3: + dependencies: + '@oxc-project/types': 0.133.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.3 + '@rolldown/binding-darwin-arm64': 1.0.3 + '@rolldown/binding-darwin-x64': 1.0.3 + '@rolldown/binding-freebsd-x64': 1.0.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 + '@rolldown/binding-linux-arm64-gnu': 1.0.3 + '@rolldown/binding-linux-arm64-musl': 1.0.3 + '@rolldown/binding-linux-ppc64-gnu': 1.0.3 + '@rolldown/binding-linux-s390x-gnu': 1.0.3 + '@rolldown/binding-linux-x64-gnu': 1.0.3 + '@rolldown/binding-linux-x64-musl': 1.0.3 + '@rolldown/binding-openharmony-arm64': 1.0.3 + '@rolldown/binding-wasm32-wasi': 1.0.3 + '@rolldown/binding-win32-arm64-msvc': 1.0.3 + '@rolldown/binding-win32-x64-msvc': 1.0.3 + + scheduler@0.27.0: {} + + source-map-js@1.2.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tslib@2.8.1: + optional: true + + typescript@5.9.3: {} + + typescript@6.0.3: {} + + undici-types@6.21.0: {} + + vite@8.0.16(@types/node@22.19.21): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.15 + rolldown: 1.0.3 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 22.19.21 + fsevents: 2.3.3 + + web-vitals@5.3.0: {} diff --git a/apps/error-tracking/cicd-monorepo-pnpm-node-react/pnpm-workspace.yaml b/apps/error-tracking/cicd-monorepo-pnpm-node-react/pnpm-workspace.yaml new file mode 100644 index 000000000..e98fd1061 --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-pnpm-node-react/pnpm-workspace.yaml @@ -0,0 +1,8 @@ +packages: + - backend + - frontend + - packages/* + +# Wait at least seven days (10080 minutes) before installing newly published +# package versions, to avoid malicious or unstable releases. +minimumReleaseAge: 10080 diff --git a/apps/error-tracking/cicd-monorepo-raw-node-react/.github/workflows/deploy-backend.yml b/apps/error-tracking/cicd-monorepo-raw-node-react/.github/workflows/deploy-backend.yml new file mode 100644 index 000000000..dbbfa583f --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-raw-node-react/.github/workflows/deploy-backend.yml @@ -0,0 +1,97 @@ +name: Deploy backend + +# Deploy pipeline for the backend only: build a Docker image from backend/, +# push it to GHCR, then SSH into the target server and run the freshly built +# image. The React/Vite frontend is intentionally left out of this workflow. + +on: + push: + branches: + - main + paths: + - 'backend/**' + - '.github/workflows/deploy-backend.yml' + workflow_dispatch: + +concurrency: + group: deploy-backend-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + name: Build and push image + runs-on: ubuntu-latest + timeout-minutes: 20 + outputs: + image: ${{ steps.meta.outputs.tags }} + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=sha,format=long + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: backend + file: backend/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy: + name: Deploy to server over SSH + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: production + steps: + - name: SSH into server and run the new image + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + envs: REGISTRY,IMAGE_NAME,GITHUB_SHA + script: | + set -euo pipefail + IMAGE="${REGISTRY}/${IMAGE_NAME}:sha-${GITHUB_SHA}" + + echo "${{ secrets.GHCR_TOKEN }}" | docker login "${REGISTRY}" -u "${{ secrets.GHCR_USER }}" --password-stdin + docker pull "${IMAGE}" + + # Replace the running container with the freshly built image. + docker rm -f backend-app 2>/dev/null || true + docker run -d \ + --name backend-app \ + --restart unless-stopped \ + -e POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}" \ + -e POSTHOG_HOST="${{ secrets.POSTHOG_HOST }}" \ + "${IMAGE}" diff --git a/apps/error-tracking/cicd-monorepo-raw-node-react/.gitignore b/apps/error-tracking/cicd-monorepo-raw-node-react/.gitignore new file mode 100644 index 000000000..d46d065c5 --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-raw-node-react/.gitignore @@ -0,0 +1,6 @@ +node_modules +dist +dist-ssr +*.log +.DS_Store +*.local diff --git a/apps/error-tracking/cicd-monorepo-raw-node-react/backend/.dockerignore b/apps/error-tracking/cicd-monorepo-raw-node-react/backend/.dockerignore new file mode 100644 index 000000000..bf43ca351 --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-raw-node-react/backend/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +*.log +.DS_Store +.env +.git +.github diff --git a/apps/error-tracking/cicd-monorepo-raw-node-react/backend/Dockerfile b/apps/error-tracking/cicd-monorepo-raw-node-react/backend/Dockerfile new file mode 100644 index 000000000..6adcee8c0 --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-raw-node-react/backend/Dockerfile @@ -0,0 +1,29 @@ +# syntax=docker/dockerfile:1 + +# ---- Build stage ---- +# Compiles the TypeScript app into dist/. +FROM node:24-alpine AS build +WORKDIR /app + +# Install all dependencies (including dev deps such as typescript) using the lockfile. +COPY package.json package-lock.json ./ +RUN npm ci + +# Compile src/ -> dist/ +COPY tsconfig.json ./ +COPY src ./src +RUN npm run build + +# ---- Runtime stage ---- +# Ships only production dependencies and the compiled output. +FROM node:24-alpine AS runtime +ENV NODE_ENV=production +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev + +COPY --from=build /app/dist ./dist + +# POSTHOG_KEY / POSTHOG_HOST are provided at runtime (e.g. `docker run -e ...`). +CMD ["node", "dist/index.js"] diff --git a/apps/error-tracking/cicd-monorepo-raw-node-react/backend/package-lock.json b/apps/error-tracking/cicd-monorepo-raw-node-react/backend/package-lock.json new file mode 100644 index 000000000..6c3f04120 --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-raw-node-react/backend/package-lock.json @@ -0,0 +1,85 @@ +{ + "name": "backend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "backend", + "version": "0.1.0", + "dependencies": { + "posthog-node": "^5.35.4" + }, + "devDependencies": { + "@types/node": "^26.0.0", + "typescript": "^5.7.2" + } + }, + "node_modules/@posthog/core": { + "version": "1.29.13", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.29.13.tgz", + "integrity": "sha512-7Me5zaeAue/wmA364Go8ChYbsVAfNAHbtDxXopWu3D6hq9PVScUcauRgjD1njgvP8NzN91SrIllE+pri3XvJVw==", + "license": "MIT", + "dependencies": { + "@posthog/types": "1.376.4" + } + }, + "node_modules/@posthog/types": { + "version": "1.376.4", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.376.4.tgz", + "integrity": "sha512-EoDEvA925lf6yxPpbP4wozlXgu4b9WEqxZlFBUDd4k2akP5R/RWyHpvQT8aYyfY6BtSLn8TnVwxPQOM4b90isA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/posthog-node": { + "version": "5.35.6", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.35.6.tgz", + "integrity": "sha512-LwoXnR89A0l75jvFrXjtsAs0BbpyCjnwY8YIUkZT91rt1YnIUfBYiLj7qoUYApdNgesgWQQHqLXxLYX59a6ZYw==", + "license": "MIT", + "dependencies": { + "@posthog/core": "1.29.13" + }, + "engines": { + "node": "^20.20.0 || >=22.22.0" + }, + "peerDependencies": { + "rxjs": "^7.0.0" + }, + "peerDependenciesMeta": { + "rxjs": { + "optional": true + } + } + }, + "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/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/apps/error-tracking/cicd-monorepo-raw-node-react/backend/package.json b/apps/error-tracking/cicd-monorepo-raw-node-react/backend/package.json new file mode 100644 index 000000000..0fcf135d9 --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-raw-node-react/backend/package.json @@ -0,0 +1,17 @@ +{ + "name": "backend", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc", + "start": "node --env-file=.env dist/index.js" + }, + "dependencies": { + "posthog-node": "^5.35.4" + }, + "devDependencies": { + "@types/node": "^26.0.0", + "typescript": "^5.7.2" + } +} diff --git a/apps/error-tracking/cicd-monorepo-raw-node-react/backend/src/index.ts b/apps/error-tracking/cicd-monorepo-raw-node-react/backend/src/index.ts new file mode 100644 index 000000000..b54898afa --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-raw-node-react/backend/src/index.ts @@ -0,0 +1,10 @@ +import { PostHog } from 'posthog-node' + +const client = new PostHog(process.env.POSTHOG_KEY ?? '', { + host: process.env.POSTHOG_HOST, +}) + +client.capture({ distinctId: 'backend', event: 'hello' }) + + +await client.shutdown() diff --git a/apps/error-tracking/cicd-monorepo-raw-node-react/backend/tsconfig.json b/apps/error-tracking/cicd-monorepo-raw-node-react/backend/tsconfig.json new file mode 100644 index 000000000..2659d22cd --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-raw-node-react/backend/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/index.html b/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/index.html new file mode 100644 index 000000000..0fca6f043 --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + frontend + + +
+ + + diff --git a/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/package-lock.json b/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/package-lock.json new file mode 100644 index 000000000..724bfdf0d --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/package-lock.json @@ -0,0 +1,1450 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "posthog-js": "^1.376.2", + "react": "^19.2.6", + "react-dom": "^19.2.6" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "typescript": "~6.0.2", + "vite": "^8.0.12" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz", + "integrity": "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.208.0.tgz", + "integrity": "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/otlp-exporter-base": "0.208.0", + "@opentelemetry/otlp-transformer": "0.208.0", + "@opentelemetry/sdk-logs": "0.208.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.208.0.tgz", + "integrity": "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/otlp-transformer": "0.208.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.208.0.tgz", + "integrity": "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/sdk-logs": "0.208.0", + "@opentelemetry/sdk-metrics": "2.2.0", + "@opentelemetry/sdk-trace-base": "2.2.0", + "protobufjs": "^7.3.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", + "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz", + "integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.208.0.tgz", + "integrity": "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", + "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", + "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@posthog/core": { + "version": "1.29.11", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.29.11.tgz", + "integrity": "sha512-/4EF7oxAFSWJgaXxppT8bdYp7MGAnWFnKz994+MetTz/T6CKbYpjqIXHCofQXtcOXjEclTYj91igA+IkVFKiSg==", + "license": "MIT", + "dependencies": { + "@posthog/types": "1.376.2" + } + }, + "node_modules/@posthog/types": { + "version": "1.376.2", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.376.2.tgz", + "integrity": "sha512-Y3ROpAxNqgcy2G0w6JoG5Gt+P6WNY2lkHTPMPzWqexRwemYbFegDi5AifDyD9/tstKTlOYKTTExtaJ5EBcghyQ==", + "license": "MIT" + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", + "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", + "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", + "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", + "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", + "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", + "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", + "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", + "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", + "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", + "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", + "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", + "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", + "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", + "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", + "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/node": { + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/react": { + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", + "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", + "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dompurify": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.5.tgz", + "integrity": "sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "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/fflate": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", + "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==", + "license": "MIT" + }, + "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/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "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/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": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/posthog-js": { + "version": "1.376.2", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.376.2.tgz", + "integrity": "sha512-Anz2pCp7dcNbammTExiZpcKC08dxfrHYaJgaXH6rq5x3Zcfj/4FkcMJF2cGCrdQXel5Y4vftiVSseZda0HAQTQ==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/api-logs": "^0.208.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.208.0", + "@opentelemetry/resources": "^2.2.0", + "@opentelemetry/sdk-logs": "^0.208.0", + "@posthog/core": "1.29.11", + "@posthog/types": "1.376.2", + "core-js": "^3.38.1", + "dompurify": "^3.3.2", + "fflate": "^0.4.8", + "preact": "^10.28.2", + "query-selector-shadow-dom": "^1.0.1", + "web-vitals": "^5.1.0" + } + }, + "node_modules/preact": { + "version": "10.29.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.2.tgz", + "integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/protobufjs": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.1.tgz", + "integrity": "sha512-4K0myLaWL5EteuSAro91EGFgcfVgxb64Jx+7oDAY6GOkXD4M69yuSEljNcInGVCA5sOPxmZ/EqDLj2x0Q0+Ygg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/query-selector-shadow-dom": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz", + "integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==", + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/rolldown": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", + "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.132.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.2", + "@rolldown/binding-darwin-arm64": "1.0.2", + "@rolldown/binding-darwin-x64": "1.0.2", + "@rolldown/binding-freebsd-x64": "1.0.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", + "@rolldown/binding-linux-arm64-gnu": "1.0.2", + "@rolldown/binding-linux-arm64-musl": "1.0.2", + "@rolldown/binding-linux-ppc64-gnu": "1.0.2", + "@rolldown/binding-linux-s390x-gnu": "1.0.2", + "@rolldown/binding-linux-x64-gnu": "1.0.2", + "@rolldown/binding-linux-x64-musl": "1.0.2", + "@rolldown/binding-openharmony-arm64": "1.0.2", + "@rolldown/binding-wasm32-wasi": "1.0.2", + "@rolldown/binding-win32-arm64-msvc": "1.0.2", + "@rolldown/binding-win32-x64-msvc": "1.0.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "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/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", + "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.2", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/web-vitals": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.2.0.tgz", + "integrity": "sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA==", + "license": "Apache-2.0" + } + } +} diff --git a/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/package.json b/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/package.json new file mode 100644 index 000000000..9a793aff5 --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/package.json @@ -0,0 +1,23 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "posthog-js": "^1.376.2", + "react": "^19.2.6", + "react-dom": "^19.2.6" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "typescript": "~6.0.2", + "vite": "^8.0.12" + } +} diff --git a/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/public/favicon.svg b/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/public/favicon.svg new file mode 100644 index 000000000..6893eb132 --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/src/App.tsx b/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/src/App.tsx new file mode 100644 index 000000000..ddf71a7dc --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/src/App.tsx @@ -0,0 +1,17 @@ +import { usePostHog } from 'posthog-js/react' + +function App() { + const posthog = usePostHog() + + return ( +
+

Vite + React + PostHog test fixture

+

This page exists to give the bundler something to ship.

+ +
+ ) +} + +export default App diff --git a/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/src/main.tsx b/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/src/main.tsx new file mode 100644 index 000000000..d302474cd --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/src/main.tsx @@ -0,0 +1,19 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { PostHogProvider } from 'posthog-js/react' +import App from './App.tsx' + +const options = { + api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST, +} + +createRoot(document.getElementById('root')!).render( + + + + + , +) diff --git a/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/tsconfig.app.json b/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/tsconfig.app.json new file mode 100644 index 000000000..7f42e5f7c --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/tsconfig.app.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/tsconfig.json b/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/tsconfig.json new file mode 100644 index 000000000..1ffef600d --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/tsconfig.node.json b/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/tsconfig.node.json new file mode 100644 index 000000000..d3c52ea64 --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "module": "esnext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/vite.config.ts b/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/vite.config.ts new file mode 100644 index 000000000..8b0f57b91 --- /dev/null +++ b/apps/error-tracking/cicd-monorepo-raw-node-react/frontend/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/apps/error-tracking/cicd-ssh-vps-node-raw/.github/workflows/deploy.yml b/apps/error-tracking/cicd-ssh-vps-node-raw/.github/workflows/deploy.yml new file mode 100644 index 000000000..711abb4e0 --- /dev/null +++ b/apps/error-tracking/cicd-ssh-vps-node-raw/.github/workflows/deploy.yml @@ -0,0 +1,44 @@ +name: Deploy + +# Deploy pipeline: no Docker. The job SSHes into the VPS and runs the build and +# start commands directly on the box — the same steps you'd type by hand after +# `ssh deploy@your-vps`. + +on: + push: + branches: + - main + workflow_dispatch: + +concurrency: + group: deploy-${{ github.ref }} + cancel-in-progress: true + +jobs: + deploy: + name: Build and run on VPS over SSH + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: production + steps: + - name: SSH into the VPS, build and run the app + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + script: | + set -euo pipefail + cd /srv/cicd-ssh-vps-node-raw + + # Pull the latest code and build it on the server. + git pull --ff-only origin main + npm ci + npm run build + + # Stop the previous instance, then start the freshly built app + # detached so it keeps running after this SSH session ends. + pkill -f "node dist/index.js" || true + POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}" \ + POSTHOG_HOST="${{ secrets.POSTHOG_HOST }}" \ + nohup node dist/index.js > app.log 2>&1 & diff --git a/apps/error-tracking/cicd-ssh-vps-node-raw/.gitignore b/apps/error-tracking/cicd-ssh-vps-node-raw/.gitignore new file mode 100644 index 000000000..753521168 --- /dev/null +++ b/apps/error-tracking/cicd-ssh-vps-node-raw/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +*.log +.DS_Store diff --git a/apps/error-tracking/cicd-ssh-vps-node-raw/package-lock.json b/apps/error-tracking/cicd-ssh-vps-node-raw/package-lock.json new file mode 100644 index 000000000..3856863ac --- /dev/null +++ b/apps/error-tracking/cicd-ssh-vps-node-raw/package-lock.json @@ -0,0 +1,85 @@ +{ + "name": "cicd-ssh-vps-node-raw", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cicd-ssh-vps-node-raw", + "version": "0.1.0", + "dependencies": { + "posthog-node": "^5.35.4" + }, + "devDependencies": { + "@types/node": "^26.0.0", + "typescript": "^5.7.2" + } + }, + "node_modules/@posthog/core": { + "version": "1.29.13", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.29.13.tgz", + "integrity": "sha512-7Me5zaeAue/wmA364Go8ChYbsVAfNAHbtDxXopWu3D6hq9PVScUcauRgjD1njgvP8NzN91SrIllE+pri3XvJVw==", + "license": "MIT", + "dependencies": { + "@posthog/types": "1.376.4" + } + }, + "node_modules/@posthog/types": { + "version": "1.376.4", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.376.4.tgz", + "integrity": "sha512-EoDEvA925lf6yxPpbP4wozlXgu4b9WEqxZlFBUDd4k2akP5R/RWyHpvQT8aYyfY6BtSLn8TnVwxPQOM4b90isA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", + "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/posthog-node": { + "version": "5.35.6", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.35.6.tgz", + "integrity": "sha512-LwoXnR89A0l75jvFrXjtsAs0BbpyCjnwY8YIUkZT91rt1YnIUfBYiLj7qoUYApdNgesgWQQHqLXxLYX59a6ZYw==", + "license": "MIT", + "dependencies": { + "@posthog/core": "1.29.13" + }, + "engines": { + "node": "^20.20.0 || >=22.22.0" + }, + "peerDependencies": { + "rxjs": "^7.0.0" + }, + "peerDependenciesMeta": { + "rxjs": { + "optional": true + } + } + }, + "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/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/apps/error-tracking/cicd-ssh-vps-node-raw/package.json b/apps/error-tracking/cicd-ssh-vps-node-raw/package.json new file mode 100644 index 000000000..a60b2d2ba --- /dev/null +++ b/apps/error-tracking/cicd-ssh-vps-node-raw/package.json @@ -0,0 +1,17 @@ +{ + "name": "cicd-ssh-vps-node-raw", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc", + "start": "node --env-file=.env dist/index.js" + }, + "dependencies": { + "posthog-node": "^5.35.4" + }, + "devDependencies": { + "@types/node": "^26.0.0", + "typescript": "^5.7.2" + } +} diff --git a/apps/error-tracking/cicd-ssh-vps-node-raw/src/index.ts b/apps/error-tracking/cicd-ssh-vps-node-raw/src/index.ts new file mode 100644 index 000000000..006dfecf3 --- /dev/null +++ b/apps/error-tracking/cicd-ssh-vps-node-raw/src/index.ts @@ -0,0 +1,10 @@ +import { PostHog } from 'posthog-node' + +const client = new PostHog(process.env.POSTHOG_KEY ?? '', { + host: process.env.POSTHOG_HOST, +}) + +client.capture({ distinctId: 'node-raw', event: 'hello' }) + + +await client.shutdown() diff --git a/apps/error-tracking/cicd-ssh-vps-node-raw/tsconfig.json b/apps/error-tracking/cicd-ssh-vps-node-raw/tsconfig.json new file mode 100644 index 000000000..2659d22cd --- /dev/null +++ b/apps/error-tracking/cicd-ssh-vps-node-raw/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/apps/error-tracking/next/.gitignore b/apps/error-tracking/next/.gitignore new file mode 100644 index 000000000..b6cb3f4d8 --- /dev/null +++ b/apps/error-tracking/next/.gitignore @@ -0,0 +1,6 @@ +node_modules +.next +out +*.log +.DS_Store +next-env.d.ts diff --git a/apps/error-tracking/next/app/layout.tsx b/apps/error-tracking/next/app/layout.tsx new file mode 100644 index 000000000..241461572 --- /dev/null +++ b/apps/error-tracking/next/app/layout.tsx @@ -0,0 +1,21 @@ +import type { Metadata } from "next"; +import { PostHogProvider } from "./providers"; + +export const metadata: Metadata = { + title: "Next App", + description: "Test fixture for PostHog source maps prompt", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} diff --git a/apps/error-tracking/next/app/page.tsx b/apps/error-tracking/next/app/page.tsx new file mode 100644 index 000000000..97af68731 --- /dev/null +++ b/apps/error-tracking/next/app/page.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { usePostHog } from "posthog-js/react"; + +export default function Home() { + const posthog = usePostHog(); + + return ( +
+

Next.js + PostHog test fixture

+

This page exists to give the bundler something to ship.

+ +
+ ); +} diff --git a/apps/error-tracking/next/app/providers.tsx b/apps/error-tracking/next/app/providers.tsx new file mode 100644 index 000000000..bbf3479e5 --- /dev/null +++ b/apps/error-tracking/next/app/providers.tsx @@ -0,0 +1,17 @@ +"use client"; + +import posthog from "posthog-js"; +import { PostHogProvider as PHProvider } from "posthog-js/react"; +import { useEffect } from "react"; + +export function PostHogProvider({ children }: { children: React.ReactNode }) { + useEffect(() => { + posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY ?? "phc_placeholder", { + api_host: + process.env.NEXT_PUBLIC_POSTHOG_HOST ?? "https://us.i.posthog.com", + capture_pageview: "history_change", + }); + }, []); + + return {children}; +} diff --git a/apps/error-tracking/next/next.config.ts b/apps/error-tracking/next/next.config.ts new file mode 100644 index 000000000..2f6491cfb --- /dev/null +++ b/apps/error-tracking/next/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + reactStrictMode: true, +}; + +export default nextConfig; diff --git a/apps/error-tracking/next/package-lock.json b/apps/error-tracking/next/package-lock.json new file mode 100644 index 000000000..cf6007f53 --- /dev/null +++ b/apps/error-tracking/next/package-lock.json @@ -0,0 +1,1405 @@ +{ + "name": "next-app", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "next-app", + "version": "0.1.0", + "dependencies": { + "next": "16.2.6", + "posthog-js": "^1.376.2", + "react": "19.1.0", + "react-dom": "19.1.0" + }, + "devDependencies": { + "@types/node": "22.10.0", + "@types/react": "19.1.0", + "@types/react-dom": "19.1.0", + "typescript": "5.7.2" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@next/env": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz", + "integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz", + "integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz", + "integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz", + "integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz", + "integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz", + "integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz", + "integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz", + "integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz", + "integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz", + "integrity": "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.208.0.tgz", + "integrity": "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/otlp-exporter-base": "0.208.0", + "@opentelemetry/otlp-transformer": "0.208.0", + "@opentelemetry/sdk-logs": "0.208.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.208.0.tgz", + "integrity": "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/otlp-transformer": "0.208.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.208.0.tgz", + "integrity": "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/sdk-logs": "0.208.0", + "@opentelemetry/sdk-metrics": "2.2.0", + "@opentelemetry/sdk-trace-base": "2.2.0", + "protobufjs": "^7.3.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", + "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz", + "integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.208.0.tgz", + "integrity": "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", + "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@posthog/core": { + "version": "1.29.11", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.29.11.tgz", + "integrity": "sha512-/4EF7oxAFSWJgaXxppT8bdYp7MGAnWFnKz994+MetTz/T6CKbYpjqIXHCofQXtcOXjEclTYj91igA+IkVFKiSg==", + "license": "MIT", + "dependencies": { + "@posthog/types": "1.376.2" + } + }, + "node_modules/@posthog/types": { + "version": "1.376.2", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.376.2.tgz", + "integrity": "sha512-Y3ROpAxNqgcy2G0w6JoG5Gt+P6WNY2lkHTPMPzWqexRwemYbFegDi5AifDyD9/tstKTlOYKTTExtaJ5EBcghyQ==", + "license": "MIT" + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", + "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.0.tgz", + "integrity": "sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-21E2zejNNRtjG4hKIyJz4aWswGEcNFTgttA0bZIRGjj1HA/tbSUxIJnIcYbn98pwJck0cS1bsQhn6eaKqbcFWw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.31", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz", + "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "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/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dompurify": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.5.tgz", + "integrity": "sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/fflate": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", + "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "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/next": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz", + "integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==", + "license": "MIT", + "dependencies": { + "@next/env": "16.2.6", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.2.6", + "@next/swc-darwin-x64": "16.2.6", + "@next/swc-linux-arm64-gnu": "16.2.6", + "@next/swc-linux-arm64-musl": "16.2.6", + "@next/swc-linux-x64-gnu": "16.2.6", + "@next/swc-linux-x64-musl": "16.2.6", + "@next/swc-win32-arm64-msvc": "16.2.6", + "@next/swc-win32-x64-msvc": "16.2.6", + "sharp": "^0.34.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "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.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/posthog-js": { + "version": "1.376.2", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.376.2.tgz", + "integrity": "sha512-Anz2pCp7dcNbammTExiZpcKC08dxfrHYaJgaXH6rq5x3Zcfj/4FkcMJF2cGCrdQXel5Y4vftiVSseZda0HAQTQ==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/api-logs": "^0.208.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.208.0", + "@opentelemetry/resources": "^2.2.0", + "@opentelemetry/sdk-logs": "^0.208.0", + "@posthog/core": "1.29.11", + "@posthog/types": "1.376.2", + "core-js": "^3.38.1", + "dompurify": "^3.3.2", + "fflate": "^0.4.8", + "preact": "^10.28.2", + "query-selector-shadow-dom": "^1.0.1", + "web-vitals": "^5.1.0" + } + }, + "node_modules/preact": { + "version": "10.29.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.2.tgz", + "integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/protobufjs": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.0.tgz", + "integrity": "sha512-LtESOsMPTZgyYtwxhvdgdjGL0HmXEaRA/hVD6sol4zA60hVXXXP/SGmxnqDbgGE8gy7pYex7cym+5vYPcmaXBQ==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/query-selector-shadow-dom": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz", + "integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==", + "license": "MIT" + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT" + }, + "node_modules/web-vitals": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.2.0.tgz", + "integrity": "sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA==", + "license": "Apache-2.0" + } + } +} diff --git a/apps/error-tracking/next/package.json b/apps/error-tracking/next/package.json new file mode 100644 index 000000000..8f41f3279 --- /dev/null +++ b/apps/error-tracking/next/package.json @@ -0,0 +1,23 @@ +{ + "name": "next-app", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "next": "16.2.6", + "posthog-js": "^1.376.2", + "react": "19.1.0", + "react-dom": "19.1.0" + }, + "devDependencies": { + "@types/node": "22.10.0", + "@types/react": "19.1.0", + "@types/react-dom": "19.1.0", + "typescript": "5.7.2" + } +} diff --git a/apps/error-tracking/next/tsconfig.json b/apps/error-tracking/next/tsconfig.json new file mode 100644 index 000000000..803331ccf --- /dev/null +++ b/apps/error-tracking/next/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/.browserslistrc b/apps/error-tracking/no-posthog-angular/angular-saas/.browserslistrc new file mode 100644 index 000000000..4f9ac2698 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/.browserslistrc @@ -0,0 +1,16 @@ +# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries + +# For the full list of supported browsers by the Angular framework, please see: +# https://angular.io/guide/browser-support + +# You can see what browsers were selected by your queries by running: +# npx browserslist + +last 1 Chrome version +last 1 Firefox version +last 2 Edge major versions +last 2 Safari major versions +last 2 iOS major versions +Firefox ESR diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/.editorconfig b/apps/error-tracking/no-posthog-angular/angular-saas/.editorconfig new file mode 100644 index 000000000..00b36a741 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/.editorconfig @@ -0,0 +1,18 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true +end_of_line = lf +max_line_length = 200 + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/.gitignore b/apps/error-tracking/no-posthog-angular/angular-saas/.gitignore new file mode 100644 index 000000000..cc7b14135 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/.gitignore @@ -0,0 +1,42 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/.husky/_/h b/apps/error-tracking/no-posthog-angular/angular-saas/.husky/_/h new file mode 100644 index 000000000..bf7c89640 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/.husky/_/h @@ -0,0 +1,22 @@ +#!/usr/bin/env sh +[ "$HUSKY" = "2" ] && set -x +n=$(basename "$0") +s=$(dirname "$(dirname "$0")")/$n + +[ ! -f "$s" ] && exit 0 + +if [ -f "$HOME/.huskyrc" ]; then + echo "husky - '~/.huskyrc' is DEPRECATED, please move your code to ~/.config/husky/init.sh" +fi +i="${XDG_CONFIG_HOME:-$HOME/.config}/husky/init.sh" +[ -f "$i" ] && . "$i" + +[ "${HUSKY-}" = "0" ] && exit 0 + +export PATH="node_modules/.bin:$PATH" +sh -e "$s" "$@" +c=$? + +[ $c != 0 ] && echo "husky - $n script failed (code $c)" +[ $c = 127 ] && echo "husky - command not found in PATH=$PATH" +exit $c diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/.husky/_/husky.sh b/apps/error-tracking/no-posthog-angular/angular-saas/.husky/_/husky.sh new file mode 100644 index 000000000..f9d063790 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/.husky/_/husky.sh @@ -0,0 +1,9 @@ +echo "husky - DEPRECATED + +Please remove the following two lines from $0: + +#!/usr/bin/env sh +. \"\$(dirname -- \"\$0\")/_/husky.sh\" + +They WILL FAIL in v10.0.0 +" \ No newline at end of file diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/.husky/_/pre-commit b/apps/error-tracking/no-posthog-angular/angular-saas/.husky/_/pre-commit new file mode 100755 index 000000000..16aae78f5 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/.husky/_/pre-commit @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +. "$(dirname "$0")/h" \ No newline at end of file diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/.husky/pre-commit b/apps/error-tracking/no-posthog-angular/angular-saas/.husky/pre-commit new file mode 100644 index 000000000..8775bd258 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/.husky/pre-commit @@ -0,0 +1 @@ +npm run format-staged diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/.stylelintrc b/apps/error-tracking/no-posthog-angular/angular-saas/.stylelintrc new file mode 100644 index 000000000..0718186ce --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/.stylelintrc @@ -0,0 +1,46 @@ +{ + "extends": ["stylelint-config-standard", "stylelint-config-recommended-scss", "stylelint-config-prettier"], + "rules": { + "font-family-name-quotes": "always-where-recommended", + "function-url-quotes": [ + "always", + { + "except": ["empty"] + } + ], + "selector-attribute-quotes": "always", + "string-quotes": "double", + "max-nesting-depth": 3, + "selector-max-compound-selectors": 3, + "selector-max-specificity": "0,3,2", + "declaration-no-important": true, + "at-rule-no-vendor-prefix": true, + "media-feature-name-no-vendor-prefix": true, + "property-no-vendor-prefix": true, + "selector-no-vendor-prefix": true, + "value-no-vendor-prefix": true, + "no-empty-source": null, + "selector-class-pattern": "[a-z-]+", + "selector-id-pattern": "[a-z-]+", + "selector-max-id": 0, + "selector-no-qualifying-type": true, + "selector-max-universal": 0, + "selector-type-no-unknown": [ + true, + { + "ignore": ["custom-elements", "default-namespace"] + } + ], + "selector-pseudo-element-no-unknown": [ + true, + { + "ignorePseudoElements": ["ng-deep"] + } + ], + "unit-allowed-list": ["px", "%", "em", "rem", "vw", "vh", "deg", "s"], + "max-empty-lines": 2, + "max-line-length": 120, + "no-invalid-position-at-import-rule": null, + "scss/comment-no-empty": null + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/.vscode/extensions.json b/apps/error-tracking/no-posthog-angular/angular-saas/.vscode/extensions.json new file mode 100644 index 000000000..77b374577 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 + "recommendations": ["angular.ng-template"] +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/.vscode/launch.json b/apps/error-tracking/no-posthog-angular/angular-saas/.vscode/launch.json new file mode 100644 index 000000000..925af8370 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ng serve", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: start", + "url": "http://localhost:4200/" + }, + { + "name": "ng test", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: test", + "url": "http://localhost:9876/debug.html" + } + ] +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/.vscode/tasks.json b/apps/error-tracking/no-posthog-angular/angular-saas/.vscode/tasks.json new file mode 100644 index 000000000..a298b5bd8 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + }, + { + "type": "npm", + "script": "test", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + } + ] +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/README.md b/apps/error-tracking/no-posthog-angular/angular-saas/README.md new file mode 100644 index 000000000..29316c183 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/README.md @@ -0,0 +1,168 @@ +#

Angular Boilerplate

+ +

+ angular + document +

+ +> ### A Fully-Configured Angular v20 Boilerplate for Enterprise-Grade Applications + +--- + +[![Netlify Status](https://api.netlify.com/api/v1/badges/3f3d0969-3549-4300-863d-06a1d6785cd0/deploy-status)](https://app.netlify.com/sites/angular-boiler-palte/deploys) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![commitizen](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg?style=flat-square)]() +[![PRs](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)]() + +## [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/arslanameer) + +- ⬇️ Older Versions + - [Angular 19 Branch](https://github.com/ArslanAmeer/angular-boilerplate/tree/angular-v19) + - [Angular 18 Branch](https://github.com/ArslanAmeer/angular-boilerplate/tree/angular-v18) + - [Angular 16 Branch](https://github.com/ArslanAmeer/angular-boilerplate/tree/angular-v16) + +Get started fast with this **Angular 20** boilerplate, crafted for **enterprise-level scalability**. It integrates critical features like **authentication**, **lazy loading**, **real-time socket communication**, **service workers**, **shell layout (header+sidebar)** making it ready to handle everything from SaaS platforms to enterprise dashboards. Built on the latest **Angular 20**, this template is packed with the newest capabilities and ensures that you adhere to **modern development best practices**. + +With a solid foundation of **PWA (Progressive Web App)**, **authentication**, **role-based permissions**, **HTTP interceptors**, **socket integration**, and more, this boilerplate takes care of the core setup so you can focus on building features. + +### Key Features: + +- [x] **Angular 20**: Built with the latest version, utilizing improved standalone components and reactive forms. +- [x] **PWA (Progressive Web App) support**: Fully configured to enable offline capabilities, background sync, and push notifications. +- [x] **Scalable folder structure**: Optimized for enterprise-level applications with a modular design that adapts to growing project needs. +- [x] **Separation of concerns**: Adopting [Domain-Driven Design (DDD)](https://en.wikipedia.org/wiki/Domain-driven_design) for a clear boundary between business logic and infrastructure code. +- [x] **Modularization**: Components, services, pipes, and directives are split into reusable modules for easy maintenance and scalability. +- [x] **Hybrid architecture**: Combines both standalone components and module-based structure, optimizing the initial app component as standalone and using modules for pages and shell. +- [x] **[Lazy loading](https://angular.io/guide/lazy-loading-ngmodules)**: Efficiently loads only the necessary modules, improving app performance. +- [x] **[Routing with guards](https://angular.io/guide/router)**: Robust routing system with [authentication and authorization guards](https://angular.io/api/router/CanActivate) for secured navigation. +- [x] **Complete authentication system**: Pre-configured JWT-based authentication, including services, guards, and interceptors for seamless integration. +- [x] **[Guards](https://angular.io/api/router/CanActivate)**: Role-based guards to protect routes and enforce permissions. +- [x] **[Interceptors](https://angular.io/api/common/http/HttpInterceptor)**: Secure API communication with interceptors handling authentication and error management. +- [x] **i18n Translation support**: Full [internationalization](https://angular.io/guide/i18n) for multi-language apps, with seamless integration of translation services. +- [x] **Basic error handling**: Centralized error handling for smooth debugging and a better user experience. +- [x] **Class-based entities**: Utilizing [Domain-Driven Design](https://en.wikipedia.org/wiki/Domain-driven_design) concepts for consistent and maintainable entity structures. +- [x] **Implementation of [Class Transformers](https://github.com/typestack/class-transformer)**: Easily transform backend data models to frontend-friendly formats (e.g., snake_case to camelCase). +- [x] **[Socket.io](https://socket.io/) integration**: Real-time updates with modular socket services that seamlessly integrate with the Angular ecosystem. +- [x] **App updates in production**: Automatic app updates for production environments, ensuring users always get the latest features and fixes without manual intervention. +- [x] **Environments for development and production**: Predefined configurations for different environments, ensuring smooth deployment and testing processes. +- [x] **Utility functions**: A set of reusable functions to handle common tasks like: + - **Local storage obfuscation**: Getters and setters that automatically encrypt and decrypt data in production environments. +- [x] **Helper functions**: Pre-configured helper functions for common development tasks, enhancing productivity and code quality. +- [x] **Detailed documentation**: Comprehensive documentation for setup, dependencies, coding styles, utility functions, and more, to help developers get up to speed quickly. +- [x] **Documented utility and helper functions**: Clearly explained and reusable across the application, with customization options. + +### Development Best Practices + +This boilerplate enforces **strict best practices** and includes a configured **ESLint flat config** to guide developers in writing clean, maintainable, and high-quality code: + +- **Code Quality**: ESLint rules enforce coding standards like naming conventions, indentation, and eliminating unused code. +- **No Direct Service Access**: Encourages a clean architecture by requiring components to interact with data via **UseCases**, keeping the service layer isolated. +- **Modularization**: Promotes single-responsibility services, with each service handling only domain-specific tasks. +- **Scoped Variables**: Enforces scoped use of variables and functions, reducing the risk of global state conflicts. +- **Clean, Maintainable Code**: The **standalone component** and **module-based** hybrid structure optimizes performance and keeps the codebase organized. +- **DRY Principle**: Adheres to **Don't Repeat Yourself (DRY)** principles, reducing redundancy and improving the maintainability of code. +- **Security Awareness**: Built-in ESLint rules ensure you avoid common security pitfalls, ensuring secure and performant code. + +### ESLint Configuration Highlights: + +- **Strict Linting Rules**: Custom **ESLint flat config** enforces coding standards, ensuring consistent formatting and type checking across the codebase. +- **Prettier Integration**: Maintains consistent code styling with automatic formatting on save or commit. +- **Error Prevention**: Detects common issues early on, such as missing type declarations or direct DOM manipulations. +- **Security Focused**: The configuration includes security-related rules to avoid risky code patterns. + +### Why Choose This Boilerplate? + +This Angular 20 boilerplate goes beyond just being a starting template—it's a **fully equipped foundation** for any **enterprise-level project**. From **fully configured authentication** and **lazy loading** to **role-based permissions** and **real-time communication**, it covers the most common requirements out-of-the-box. + +By enforcing **best coding practices** and strict **ESLint rules**, the boilerplate guarantees that every contributor to your project adheres to **high development standards**. Plus, the **modular, scalable architecture** and **cutting-edge Angular features** ensure that this project grows with you. + +Whether you’re developing a SaaS platform, building a dynamic web app, or designing an enterprise-level solution, this boilerplate gives you the tools you need—fast. + +### Continuous Updates + +This project will be regularly updated with the latest **Angular** releases, ensuring compatibility with the latest features and security updates. Stay tuned for new enhancements and features as they are added based on evolving development needs. + +### Demo: + +Check out working demo [here](https://angular-boilerplate.arslanameer.com) + +> ## **Note:** You can remove this with all the above lines and use rest in your documentation. + +--- + +# Web Front-End + +## Status Badges + +

 

+ +## 💻 Current Stack Version: + +- Node `^v22` +- Angular `^v20` + +

 

+ +## 🚀 Project setup + +If you want to setup this project locally and start developing, read setup and developers guide here: [Setup](docs/setup.md) + +

 

+ +## 😎 Coding style + +We make use of **[Javascript Standard Style](https://standardjs.com/)** while developing. + +You can integrate it with [eslint](https://eslint.org/) linter tool in your IDE to help smoothen the process by integrating automatic linting in compile time. + +We already have a `eslint.config.js` file in the root of the project which you can use to configure your IDE. + +We also have husky hooks with prettier and eslint to make sure that the code is linted and formatted before committing. + +If you want to read more about the rules, read [Coding style](docs/coding-style.md) + +

 

+ +## 🧳 Dependencies + +If you want to read more about dependencies in this platform, read [Dependencies](docs/dependencies.md) + +

 

+ +## 🪢 Helper and Utility functions + +We have several documented helper and utility functions that play a big part of the platform. They are available in `@core\helpers` and `@core\utils` folder. + +

 

+ +--- + +## 📜 Change Logs: + +_(Latest)_ + +

 

+ +## [Version 0.0.0] - 2025-07-20 + +### Added + +### Changed + +### Fixed + +--- + +

 

+ +_You can see all change logs [Here](/CHANGELOG.md)._ + +

 

+ +--- + +Author: + +[Arslan Ameer](www.arslanameer.com) | [GitHub](https://github.com/ArslanAmeer) + +[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/arslanameer) diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/angular.json b/apps/error-tracking/no-posthog-angular/angular-saas/angular.json new file mode 100644 index 000000000..7df7bbcde --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/angular.json @@ -0,0 +1,140 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "angular-boilerplate": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss", + "skipTests": true, + "changeDetection": "OnPush" + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "outputPath": "dist/angular-boilerplate", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js", "@angular/localize/init", "reflect-metadata"], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.scss"], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "1MB", + "maximumError": "3MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "5KB", + "maximumError": "8kB" + } + ], + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "outputHashing": "all", + "serviceWorker": "ngsw-config.json" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "angular-boilerplate:build:production" + }, + "development": { + "buildTarget": "angular-boilerplate:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular/build:extract-i18n" + }, + "test": { + "builder": "@angular/build:karma", + "options": { + "polyfills": ["zone.js", "zone.js/testing", "@angular/localize/init"], + "tsConfig": "tsconfig.spec.json", + "inlineStyleLanguage": "scss", + "assets": [ + { + "glob": "**/*", + "input": "public" + }, + "src/robots.txt", + "src/assets" + ], + "styles": ["src/styles.scss"], + "scripts": [] + } + }, + "lint": { + "builder": "@angular-eslint/builder:lint", + "options": { + "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"] + } + } + } + } + }, + "cli": { + "analytics": false, + "schematicCollections": ["@angular-eslint/schematics"] + }, + "schematics": { + "@schematics/angular:component": { + "type": "component" + }, + "@schematics/angular:directive": { + "type": "directive" + }, + "@schematics/angular:service": { + "type": "service" + }, + "@schematics/angular:guard": { + "typeSeparator": "." + }, + "@schematics/angular:interceptor": { + "typeSeparator": "." + }, + "@schematics/angular:module": { + "typeSeparator": "." + }, + "@schematics/angular:pipe": { + "typeSeparator": "." + }, + "@schematics/angular:resolver": { + "typeSeparator": "." + } + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/docs/coding-style.md b/apps/error-tracking/no-posthog-angular/angular-saas/docs/coding-style.md new file mode 100644 index 000000000..eb21016c9 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/docs/coding-style.md @@ -0,0 +1,180 @@ +# Coding Style + +## Coding style + +For coding style and linter we use **[Javascript Standard Style](https://standardjs.com/)** + +Despite what is indicated in Standard.js webpage, we don't use as standard a package, but trying to follow the rules for best practices. We are planning to use +[@angular-eslint/schematic](https://github.com/angular-eslint/angular-eslint) + +## Why + +Javascript standard style **[isn't a real web standard](https://standardjs.com/index.html#but-this-isnt-a-real-web-standard)**, but this rules helps to keep the code to a high +_standard of quality_ and ensures that all contributors follow some basic coding style. + +A [lot of companies](https://standardjs.com/index.html#who-uses-javascript-standard-style) around the world are using standard.js + +## Rules (brief) + +- Use 2 spaces for indentation. +- Use single quotes for strings except to avoid escaping. +- Use camelcase when naming variables and functions. +- Add a space after keywords. +- Trailing commas not allowed. +- Add a space before a function declaration's parentheses. +- Always use === instead of ==. +- Keep else statements on the same line as their curly braces. +- Multiple blank lines not allowed. +- For var declarations, write each declaration in its own statement. +- **No semicolons.** (Currently we are using semicolons, but we are planning to remove them) + +Check full _standard.js_ rules at [standard.js rules page](https://standardjs.com/rules.html) + +## Typescript + +The project uses both ES and Typescript. Config for Typescript compiler is _allowJs = true_ + +### Code organization + +We use `pages` folder in `app` folder to manage all the views/screens. +And `_shared` folder within `pages` folder containing views.pages that are shared across multiple users. + +```console + +├───app +│ ├───@core +│ │ ├───constants +│ │ │ └─── constants set in the whole +│ │ ├───entities +│ │ │ └─── Entity models used in the app +│ │ ├───enums +│ │ │ └─── enums files to be used in the app +│ │ ├───guards +│ │ │ └─── addition guards. Auth related guards are in auth module. +│ │ ├───helpers +│ │ │ └─── helpers files to support the app +│ │ ├───interceptors +│ │ │ └─── interceptors files to intercept the requests +│ │ ├───interfaces +│ │ │ └─── interfaces files to be used in the app +│ │ ├───services +│ │ │ └─── services files used to fetch data from the backend only. they only contain http requests. +│ │ ├───socket-io +│ │ │ └─── contains socket service to be used in the app +│ │ ├───usecases +│ │ │ └─── usecases/compositions files used to get data from the services, mutate or manipulate and pass it to the views. +│ │ └───utils +│ │ └─── utils files containing utility functions +│ ├───auth (complete independent auth module) +│ │ ├───enums +│ │ │ └─── enums files to be used in the auth module only. +│ │ ├───guard +│ │ │ ├─── auth guard +│ │ │ └─── permission guard +│ │ ├───login +│ │ │ └─── login page as default page for the app. +│ │ ├───logout +│ │ │ └─── logout page. (can be called via route) +│ │ └───services +│ │ ├─── authentication service: to handle login, logout. +│ │ ├─── credentials service: to handle authenticated state and credentials stored in localstorage. +│ │ └─── permission serice: to handle permissions. +│ ├───i18n +│ │ ├───i18n service to handle language seletion +│ │ └───language selector component +│ ├───pages +│ │ ├───pages.module +│ │ │ └─── import all the pages in this module. +│ │ └───pages-routing +│ │ └─── Main route file for screen/views/pages. Lazy load other screen, componenet, module etc in this route file +│ ├───shared +│ │ ├───components +│ │ │ └─── Shared Components to be able to use in whole app (for reusability) +│ │ ├───directives +│ │ │ └─── Shared Directives to be available in whole app (for reusability) +│ │ └───pipes +│ │ └─── Shared Pipes to be available in whole app (for reusability) +│ └───shell +│ │ ├───components +│ │ │ └─── Shell Components to be able to use in whole app (for reusability) such as header, footer, sidebar etc. +│ └───└───services +│ └─── shell service: to handle shell related data including sidebar toggle state, handle child routes etc. +│ └─── theme service: to handle theme related data including theme change, theme selection etc. +│ +├───theme +│ └─── containes all the theme related files including scss files +└───environments +``` + +### Structure Explained + +- **Core**: + As the backbone of the application, the `@core` folder contains essential components, services, and utilities for the app’s overall functionality. + + - **Constants**: Contains constant values that are used throughout the app, similar to app settings or configurations. + - **Entities**: Holds entity models for data manipulation, representing core business objects used throughout the app. + - **Enums**: Stores all enums to ensure type safety and centralized enum management. + - **Guards**: Contains additional guards for protecting routes and handling authorization logic. + - **Helpers**: Utility functions or services like authentication helpers, query builders, error handlers, etc. + - **Interceptors**: Holds files that intercept and manipulate HTTP requests or responses. + - **Interfaces**: Centralized location for interface declarations across the app for consistent data contracts. + - **Services**: This folder includes services dedicated to fetching or sending data to/from the backend through HTTP requests. Services here are aligned with the Single Responsibility Principle. + - **Socket IO**: Contains a socket service to handle WebSocket communications across the app. + - **Usecases**: These are composition functions that act as the intermediary between services and views. They manipulate or transform data fetched from services before passing it to the UI. + - **Utils**: Utility functions like validators, transformers, or helper functions that don’t depend on specific app features. + +- **Auth** (Independent module): + A completely independent authentication module. + + - **Enums**: Manages enums specific to authentication. + - **Guards**: Contains guards related to authentication, like the auth guard and permission guard. + - **Login**: The login page that serves as the app’s default landing page. + - **Logout**: The logout page, accessible via route to terminate sessions. + - **Services**: Contains services related to authentication, managing user sessions, credentials, and permissions. + - Authentication Service: Handles login and logout functionality. + - Credentials Service: Manages session state and user data in local storage. + - Permission Service: Handles user access levels and permissions. + +- **i18n**: + Manages internationalization, including: + + - **i18n Service**: Handles language selection and translation. + - **Language Selector Component**: A reusable component to switch between languages in the app. + +- **Pages**: + Manages all views/screens shown to the user. + + - **Pages Module**: The module approach used to group related screens into modules that are lazy-loaded. Each major section or feature can have its own module, and these modules are imported into the shell module. + - **Pages Routing**: Main routing file for the app's screens. It handles lazy-loading of other modules, components, and views to improve performance. + +- **Shared**: + This folder contains reusable components, directives, and pipes that can be used across the entire application. + + - **Components**: Shared components like buttons, forms, or other UI elements that can be used across multiple views. + - **Directives**: Shared directives that can be used across multiple modules. + - **Pipes**: Reusable pipes for formatting or transforming data within the app. + +- **Shell**: + Manages the core layout components that are used throughout the app. + + - **Components**: Reusable components like the header, footer, sidebar, etc., which make up the app’s structure. + - **Services**: + - **Shell Service**: Manages data related to the layout, like the sidebar toggle state or handling child routes. + - **Theme Service**: Manages app themes, including theme switching and selection. + +- **Assets**: + Contains static files, such as images, icons, and language files, used across the app. + +- **Theme**: + This folder contains all SCSS files responsible for managing the application's theme and styles. + + - **\_base**: Handles basic style definitions and resets. + - **\_layout**: Manages layout-specific styles such as grids, containers, and spacing. + - **\_mixin**: Defines reusable SCSS mixins for consistent styling across the app. + - **\_overrides**: Contains styles that override third-party or default styles. + - **\_utilities**: Utility classes for common tasks like margin, padding, or text alignment. + - **\_variables**: Stores SCSS variables for colors, fonts, breakpoints, etc. + - **Main theme file**: Imports all the partials and applies the complete theme to the app, handling customization and styling of various components. + +- **Environments**: + Stores environment-specific configurations like API endpoints for different builds (development, production, etc.). diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/docs/dependencies.md b/apps/error-tracking/no-posthog-angular/angular-saas/docs/dependencies.md new file mode 100644 index 000000000..623268763 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/docs/dependencies.md @@ -0,0 +1,29 @@ +# Dependencies + +This app has third-party dependencies (both production and development). These dependencies are listed in `package.json`. + +## Highlighted dependencies + +- [@ngneat/hot-toast](https://github.com/ngneat/hot-toast): Toast notifications for Angular. +- [@ngneat/overview](https://github.com/ngneat/overview): A powerful solution for Angular app walkthroughs. +- [@ngneat/until-destroy](https://github.com/ngneat/until-destroy): Automatic cleanup of Angular subscriptions. +- [@ngx-translate/core](https://www.npmjs.com/package/@ngx-translate/core): Internationalization (i18n) library for Angular applications. +- [@ngx-translate/http-loader](https://www.npmjs.com/package/@ngx-translate/http-loader): A library to load translations from HTTP requests. +- [bootstrap](https://www.npmjs.com/package/bootstrap): Popular front-end framework for responsive design. +- [sanitize.css](https://github.com/csstools/sanitize.css): A modern CSS reset. +- [socket.io-client](https://www.npmjs.com/package/socket.io-client): Socket.IO client for real-time bidirectional communication. + +## Dev Dependencies + +- [@eslint/js](https://www.npmjs.com/package/@eslint/js): ESLint’s official JavaScript rules. +- [eslint](https://www.npmjs.com/package/eslint): JavaScript/TypeScript linter. +- [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier): Turns off ESLint rules that conflict with Prettier. +- [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier): Runs Prettier as an ESLint rule. +- [husky](https://www.npmjs.com/package/husky): Git hooks tool to enforce code standards. +- [karma](https://www.npmjs.com/package/karma): Test runner for JavaScript. +- [prettier](https://www.npmjs.com/package/prettier): Code formatter for consistent style. +- [pretty-quick](https://www.npmjs.com/package/pretty-quick): Runs Prettier on staged files. +- [stylelint](https://www.npmjs.com/package/stylelint): Linter for CSS/SCSS. +- [stylelint-config-prettier](https://github.com/prettier/stylelint-config-prettier): Disables stylelint rules that conflict with Prettier. +- [typescript](https://www.npmjs.com/package/typescript): Superset of JavaScript that adds static types. +- [ts-node](https://www.npmjs.com/package/ts-node): TypeScript execution environment for Node.js. diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/docs/setup.md b/apps/error-tracking/no-posthog-angular/angular-saas/docs/setup.md new file mode 100644 index 000000000..1e663e662 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/docs/setup.md @@ -0,0 +1,119 @@ +# Project Setup + +## Requirements + +- Node 20+ +- Yarn 1.22+ + +## Install dependencies + +To install all dependencies, run: + +```bash +yarn install +``` + +### Compiles and serve for development + +```bash +yarn start +``` + +### Compiles and serve for development with app accessible over network + +```bash +yarn serve +``` + +### Compiles and minifies for production + +```bash +yarn build +``` + +### Lints and fixes files + +```bash +yarn lint +``` + +--- + +## Development Guide + +This guide provides an overview of key aspects of the project structure and helpful instructions for development. + +### Folder Structure + +- The project follows a modular architecture with key directories like `@core`, `pages`, `shared`, `shell`, and `theme`. +- The `@core` folder contains essential services, guards, utilities, and helpers needed for the functioning of the entire application. +- The `shared` folder includes reusable components, directives, and pipes, making it easier to reuse code across the app. +- The `theme` folder includes all SCSS files needed for styling, such as `_base`, `_layout`, `_mixin`, `_overrides`, `_utilities`, and `_variables`. This allows for easy customization of themes and styles. +- The `public` folder in root contains all static assets like images, icons, favicons, meta etc. + +### Key Development Points + +- **Todos**: The application has multiple TODO comments scattered across the codebase. You may want to review these and update them as per your requirements and customization needs. + +- **Roles**: User roles are handled as an array fetched from the backend. Ensure your role-based logic reflects this, particularly in services and guards. + +- **Icons**: No icon library is included in the project. We generally use SVG icons from the `public` folder. You can add custom icons as required. Specifically, for the sidebar navigation, there’s an `icon` property in the nav items interface where you can specify the icons for different routes. + +- **SCSS**: Basic SCSS for the layout is already provided, but it’s built based on enterprise-level use cases. You will likely need to modify the styles according to your design guidelines or application needs. Key SCSS files include: + - `_base`: Contains base styles. + - `_layout`: Manages layout styles like grids and spacing. + - `_mixin`: Reusable mixins for common styles. + - `_overrides`: Overrides default or third-party styles. + - `_utilities`: Utility classes for quick styling (e.g., margins, paddings). + - `_variables`: Central place for colors, breakpoints, and other SCSS variables. + +### API Integration Flow + +- The app uses a best practice approach to manage API integration. This demo flow is available under the **randomUser** feature. + +- **Service**: Each service is responsible for handling one aspect of the application. For example, the **UserService** will only handle user-related API calls. This ensures the **Single Responsibility Principle** is followed. The service functions should be aligned with HTTP request methods such as `find`, `get`, `put`, `create`, and `delete`. + +- **Use Cases**: The service is then consumed in the **UseCase** layer. A use case file is responsible for processing the data fetched from the service. You can perform any data mutation, calculation, or transformation inside the use case. The goal is to avoid direct access to services from components. + + Example: + + ```typescript + const useUser = new UseUser(); + useUser.findUsers(); // Calls the corresponding function in the UseUser use case + ``` + + This modular approach enhances the separation of concerns and ensures that components interact with use cases rather than services directly. + +- **Components**: In your components, you will call use case methods for data instead of calling services directly. This modular approach ensures better structure and maintainability. + +### Application Architecture + +- **Shell Layout**: The app uses a shell structure with components for the header, sidebar, and footer. These components provide the main layout for the app. + +- **Auth Setup**: Authentication is entirely modular, with dedicated services, guards, and modules. The authentication system is independent of the rest of the application, so you can make updates to the auth system without affecting other modules. + +- **Modules**: The app uses both standalone and modular approach where core sections are split into different modules. For example, the shell and pages are separate module to optimize performance. +- your can add new pages and views either lazy loaded modules or standalone components. + +### Predefined Assets + +- The project includes several predefined assets, including: + - **SCSS files** for theming and layouts. + - **Font files** for consistent typography. + - **Meta files** to ensure proper SEO and page metadata are set. + +### Core and Shared Items + +- **Utility Functions**: The `@core/utils` folder contains various utility functions, including data transformers, validators, and object utilities. + +- **Theme Service**: The app includes a `ThemeService` that allows for dynamic theme changes, such as switching between light and dark themes. You can modify this service to update CSS variables or apply custom themes. + +### Additional Notes + +- **Customization**: Since the app provides a solid foundational structure, you'll need to customize various aspects like SCSS, iconography, and utility services based on your project's requirements. + +- **Architecture Flexibility**: The modularity of the app allows for easy scaling and modification without affecting the core architecture. You can add new features, modules, or components as needed while maintaining code separation and reusability. + +--- + +This updated setup and development guide should now reflect all the important details regarding API integration and folder structures. Let me know if you'd like any further refinements! diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/eslint.config.js b/apps/error-tracking/no-posthog-angular/angular-saas/eslint.config.js new file mode 100644 index 000000000..6a2e6181f --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/eslint.config.js @@ -0,0 +1,126 @@ +// @ts-check +const eslint = require('@eslint/js'); +const tseslint = require('typescript-eslint'); +const angular = require('angular-eslint'); +const prettier = require('prettier'); +const eslintPluginPrettierRecommended = require('eslint-plugin-prettier/recommended'); + +module.exports = tseslint.config( + { + files: ['**/*.ts'], + languageOptions: { + parserOptions: { + project: './tsconfig.json', + }, + }, + extends: [eslint.configs.recommended, ...tseslint.configs.strict, ...tseslint.configs.stylistic, ...angular.configs.tsRecommended, eslintPluginPrettierRecommended], + processor: angular.processInlineTemplates, + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'app', + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: 'app', + style: 'kebab-case', + }, + ], + '@angular-eslint/no-empty-lifecycle-method': 'error', + 'max-len': [ + 'error', + { + code: 200, + }, + ], + 'no-console': [ + 'error', + { + allow: ['warn', 'error', 'log'], + }, + ], + quotes: ['error', 'single'], + '@typescript-eslint/prefer-readonly': 'error', + '@typescript-eslint/no-non-null-assertion': 'error', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + args: 'none', + }, + ], + 'no-duplicate-imports': 'error', + 'no-var': 'error', + '@angular-eslint/no-conflicting-lifecycle': 'error', + '@angular-eslint/no-output-native': 'error', + '@angular-eslint/no-inputs-metadata-property': 'error', + '@angular-eslint/no-outputs-metadata-property': 'error', + '@angular-eslint/no-host-metadata-property': 'error', + '@angular-eslint/no-input-rename': 'error', + '@angular-eslint/no-output-rename': 'error', + '@angular-eslint/use-lifecycle-interface': 'error', + '@angular-eslint/use-pipe-transform-interface': 'error', + '@angular-eslint/component-class-suffix': 'error', + '@angular-eslint/directive-class-suffix': 'error', + '@angular-eslint/contextual-lifecycle': 'error', + 'no-empty-function': 'off', + '@typescript-eslint/no-empty-function': [ + 'error', + { + allow: ['constructors'], + }, + ], + '@typescript-eslint/member-ordering': [ + 'error', + { + default: [ + 'public-static-field', + 'protected-static-field', + 'private-static-field', + 'public-instance-field', + 'protected-instance-field', + 'private-instance-field', + 'public-static-method', + 'protected-static-method', + 'private-static-method', + 'constructor', + 'public-instance-method', + 'protected-instance-method', + 'private-instance-method', + ], + }, + ], + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/naming-convention': [ + 'error', + { + selector: 'memberLike', + modifiers: ['private'], + format: ['camelCase'], + leadingUnderscore: 'require', + }, + { + selector: 'enumMember', + format: null, + }, + ], + '@typescript-eslint/explicit-member-accessibility': [ + 'off', + { + accessibility: 'no-public', + }, + ], + '@typescript-eslint/no-extraneous-class': 'off', + }, + }, + { + files: ['**/*.html'], + extends: [...angular.configs.templateRecommended], + rules: {}, + }, +); diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/ngsw-config.json b/apps/error-tracking/no-posthog-angular/angular-saas/ngsw-config.json new file mode 100644 index 000000000..46f55dc27 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/ngsw-config.json @@ -0,0 +1,21 @@ +{ + "$schema": "./node_modules/@angular/service-worker/config/schema.json", + "index": "/index.html", + "assetGroups": [ + { + "name": "app", + "installMode": "prefetch", + "resources": { + "files": ["/favicon.ico", "/index.html", "/manifest.webmanifest", "/*.css", "/*.js"] + } + }, + { + "name": "assets", + "installMode": "lazy", + "updateMode": "prefetch", + "resources": { + "files": ["/**/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"] + } + } + ] +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/package-lock.json b/apps/error-tracking/no-posthog-angular/angular-saas/package-lock.json new file mode 100644 index 000000000..3034213a2 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/package-lock.json @@ -0,0 +1,11939 @@ +{ + "name": "angular-boilerplate", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "angular-boilerplate", + "version": "0.0.0", + "dependencies": { + "@angular/animations": "^21.0.0", + "@angular/common": "^21.0.0", + "@angular/compiler": "^21.0.0", + "@angular/core": "^21.0.0", + "@angular/forms": "^21.0.0", + "@angular/localize": "^21.0.0", + "@angular/platform-browser": "^21.0.0", + "@angular/platform-browser-dynamic": "^21.0.0", + "@angular/router": "^21.0.0", + "@angular/service-worker": "^21.0.0", + "@colsen1991/ngx-translate-extract-marker": "^3.0.1", + "@ngx-translate/core": "^16.0.4", + "@ngx-translate/http-loader": "^16.0.1", + "@ngxpert/hot-toast": "^6.1.0", + "algoliasearch": "^5.46.2", + "bootstrap": "^5.3.0", + "class-transformer": "^0.5.1", + "package.json": "^2.0.1", + "reflect-metadata": "^0.2.0", + "rxjs": "^7.8.2", + "sanitize.css": "^13.0.0", + "socket.io-client": "^4.8.1", + "zone.js": "~0.15.1" + }, + "devDependencies": { + "@angular/build": "^21.0.0", + "@angular/cli": "^21.0.0", + "@angular/compiler-cli": "^21.0.0", + "@angular/language-service": "^21.0.0", + "@types/jasmine": "~5.1.0", + "angular-eslint": "^21.0.0", + "eslint": "^9.30.0", + "jasmine-core": "~5.2.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "typescript": "~5.9.0", + "typescript-eslint": "^8.33.1" + } + }, + "node_modules/@algolia/abtesting": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.14.0.tgz", + "integrity": "sha512-cZfj+1Z1dgrk3YPtNQNt0H9Rr67P8b4M79JjUKGS0d7/EbFbGxGgSu6zby5f22KXo3LT0LZa4O2c6VVbupJuDg==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.0", + "@algolia/requester-browser-xhr": "5.48.0", + "@algolia/requester-fetch": "5.48.0", + "@algolia/requester-node-http": "5.48.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.48.0.tgz", + "integrity": "sha512-n17WSJ7vazmM6yDkWBAjY12J8ERkW9toOqNgQ1GEZu/Kc4dJDJod1iy+QP5T/UlR3WICgZDi/7a/VX5TY5LAPQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.0", + "@algolia/requester-browser-xhr": "5.48.0", + "@algolia/requester-fetch": "5.48.0", + "@algolia/requester-node-http": "5.48.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.48.0.tgz", + "integrity": "sha512-v5bMZMEqW9U2l40/tTAaRyn4AKrYLio7KcRuHmLaJtxuJAhvZiE7Y62XIsF070juz4MN3eyvfQmI+y5+OVbZuA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.0", + "@algolia/requester-browser-xhr": "5.48.0", + "@algolia/requester-fetch": "5.48.0", + "@algolia/requester-node-http": "5.48.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.48.0.tgz", + "integrity": "sha512-7H3DgRyi7UByScc0wz7EMrhgNl7fKPDjKX9OcWixLwCj7yrRXDSIzwunykuYUUO7V7HD4s319e15FlJ9CQIIFQ==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.48.0.tgz", + "integrity": "sha512-tXmkB6qrIGAXrtRYHQNpfW0ekru/qymV02bjT0w5QGaGw0W91yT+53WB6dTtRRsIrgS30Al6efBvyaEosjZ5uw==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.0", + "@algolia/requester-browser-xhr": "5.48.0", + "@algolia/requester-fetch": "5.48.0", + "@algolia/requester-node-http": "5.48.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.48.0.tgz", + "integrity": "sha512-4tXEsrdtcBZbDF73u14Kb3otN+xUdTVGop1tBjict+Rc/FhsJQVIwJIcTrOJqmvhtBfc56Bu65FiVOnpAZCxcw==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.0", + "@algolia/requester-browser-xhr": "5.48.0", + "@algolia/requester-fetch": "5.48.0", + "@algolia/requester-node-http": "5.48.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.48.0.tgz", + "integrity": "sha512-unzSUwWFpsDrO8935RhMAlyK0Ttua/5XveVIwzfjs5w+GVBsHgIkbOe8VbBJccMU/z1LCwvu1AY3kffuSLAR5Q==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.0", + "@algolia/requester-browser-xhr": "5.48.0", + "@algolia/requester-fetch": "5.48.0", + "@algolia/requester-node-http": "5.48.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.48.0.tgz", + "integrity": "sha512-RB9bKgYTVUiOcEb5bOcZ169jiiVW811dCsJoLT19DcbbFmU4QaK0ghSTssij35QBQ3SCOitXOUrHcGgNVwS7sQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.0", + "@algolia/requester-browser-xhr": "5.48.0", + "@algolia/requester-fetch": "5.48.0", + "@algolia/requester-node-http": "5.48.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.48.0.tgz", + "integrity": "sha512-rhoSoPu+TDzDpvpk3cY/pYgbeWXr23DxnAIH/AkN0dUC+GCnVIeNSQkLaJ+CL4NZ51cjLIjksrzb4KC5Xu+ktw==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.0", + "@algolia/requester-browser-xhr": "5.48.0", + "@algolia/requester-fetch": "5.48.0", + "@algolia/requester-node-http": "5.48.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.48.0.tgz", + "integrity": "sha512-aSe6jKvWt+8VdjOaq2ERtsXp9+qMXNJ3mTyTc1VMhNfgPl7ArOhRMRSQ8QBnY8ZL4yV5Xpezb7lAg8pdGrrulg==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.0", + "@algolia/requester-browser-xhr": "5.48.0", + "@algolia/requester-fetch": "5.48.0", + "@algolia/requester-node-http": "5.48.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.48.0.tgz", + "integrity": "sha512-p9tfI1bimAaZrdiVExL/dDyGUZ8gyiSHsktP1ZWGzt5hXpM3nhv4tSjyHtXjEKtA0UvsaHKwSfFE8aAAm1eIQA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.0", + "@algolia/requester-browser-xhr": "5.48.0", + "@algolia/requester-fetch": "5.48.0", + "@algolia/requester-node-http": "5.48.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.48.0.tgz", + "integrity": "sha512-XshyfpsQB7BLnHseMinp3fVHOGlTv6uEHOzNK/3XrEF9mjxoZAcdVfY1OCXObfwRWX5qXZOq8FnrndFd44iVsQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.48.0.tgz", + "integrity": "sha512-Q4XNSVQU89bKNAPuvzSYqTH9AcbOOiIo6AeYMQTxgSJ2+uvT78CLPMG89RIIloYuAtSfE07s40OLV50++l1Bbw==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.48.0.tgz", + "integrity": "sha512-ZgxV2+5qt3NLeUYBTsi6PLyHcENQWC0iFppFZekHSEDA2wcLdTUjnaJzimTEULHIvJuLRCkUs4JABdhuJktEag==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.2101.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2101.3.tgz", + "integrity": "sha512-vKz8aPA62W+e9+pF6ct4CRDG/MjlIH7sWFGYkxPPRst2g46ZQsRkrzfMZAWv/wnt6OZ1OwyRuO3RW83EMhag8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "21.1.3", + "rxjs": "7.8.2" + }, + "bin": { + "architect": "bin/cli.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.1.3.tgz", + "integrity": "sha512-huEXd1tWQHwwN+0VGRT+vSVplV0KNrGFUGJzkIW6iJE1SQElxn6etMai+pSd5DJcePkx6+SuscVsxbfwf70hnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.3", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.1.3.tgz", + "integrity": "sha512-Ps7bRl5uOcM7WpNJHbSls/jz5/wAI0ldkTlKyiBFA7RtNeQIABAV+hvlw5DJuEb1Lo5hnK0hXj90AyZdOxzY+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "21.1.3", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.21", + "ora": "9.0.0", + "rxjs": "7.8.2" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-eslint/builder": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.2.0.tgz", + "integrity": "sha512-wcp3J9cbrDwSeI/o1D/DSvMQa8zpKjc5WhRGTx33omhWijCfiVNEAiBLWiEx5Sb/dWcoX8yFNWY5jSgFVy9Sjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": ">= 0.2100.0 < 0.2200.0", + "@angular-devkit/core": ">= 21.0.0 < 22.0.0" + }, + "peerDependencies": { + "@angular/cli": ">= 21.0.0 < 22.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/bundled-angular-compiler": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-21.2.0.tgz", + "integrity": "sha512-J0DWL+j6t9ItFIyIADvzHGqwDA1qfVJ9bx+oTmJ/Hlo7cUpIRoXpcTXpug0CEEABFH0RfDu6PDG2b0FoZ1+7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular-eslint/eslint-plugin": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-21.2.0.tgz", + "integrity": "sha512-X2Qn2viDsjm91CEMxNrxDH3qkKpp6un0C1F1BW2p/m9J4AUVfOcXwWz9UpHFSHTRQ+YlTJbiH1ZwwAPeKhFaxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "21.2.0", + "@angular-eslint/utils": "21.2.0", + "ts-api-utils": "^2.1.0" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/eslint-plugin-template": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-21.2.0.tgz", + "integrity": "sha512-lJ13Dj0DjR6YiceQR0sRbyWzSzOQ6uZPwK9CJUF3wuZjYAUvL1D61zaU9QrVLtf89NVOxv+dYZHDdu3IDeIqbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "21.2.0", + "@angular-eslint/utils": "21.2.0", + "aria-query": "5.3.2", + "axobject-query": "4.1.0" + }, + "peerDependencies": { + "@angular-eslint/template-parser": "21.2.0", + "@typescript-eslint/types": "^7.11.0 || ^8.0.0", + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/schematics": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-21.2.0.tgz", + "integrity": "sha512-WtT4fPKIUQ/hswy+l2GF/rKOdD+42L3fUzzcwRzNutQbe2tU9SimoSOAsay/ylWEuhIOQTs7ysPB8fUgFQoLpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": ">= 21.0.0 < 22.0.0", + "@angular-devkit/schematics": ">= 21.0.0 < 22.0.0", + "@angular-eslint/eslint-plugin": "21.2.0", + "@angular-eslint/eslint-plugin-template": "21.2.0", + "ignore": "7.0.5", + "semver": "7.7.3", + "strip-json-comments": "3.1.1" + }, + "peerDependencies": { + "@angular/cli": ">= 21.0.0 < 22.0.0" + } + }, + "node_modules/@angular-eslint/template-parser": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-21.2.0.tgz", + "integrity": "sha512-TCb3qYOC/uXKZCo56cJ6N9sHeWdFhyVqrbbYfFjTi09081T6jllgHDZL5Ms7gOMNY8KywWGGbhxwvzeA0RwTgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "21.2.0", + "eslint-scope": "^9.0.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/utils": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-21.2.0.tgz", + "integrity": "sha512-E19/hkuvHoNFvctBkmEiGWpy2bbC6cgbr3GNVrn2nGtbI4jnwnDFCGHv50I4LBfvj0PA9E6TWe73ejJ5qoMJWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "21.2.0" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, + "node_modules/@angular/animations": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.1.3.tgz", + "integrity": "sha512-UADMncDd9lkmIT1NPVFcufyP5gJHMPzxNaQpojiGrxT1aT8Du30mao0KSrB4aTwcicv6/cdD5bZbIyg+FL6LkQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/core": "21.1.3" + } + }, + "node_modules/@angular/build": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.1.3.tgz", + "integrity": "sha512-RXVRuamfrSPwsFCLJgsO2ucp+dwWDbGbhXrQnMrGXm0qdgYpI8bAsBMd8wOeUA6vn4fRmjaRFQZbL/rcIVrkzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.2101.3", + "@babel/core": "7.28.5", + "@babel/helper-annotate-as-pure": "7.27.3", + "@babel/helper-split-export-declaration": "7.24.7", + "@inquirer/confirm": "5.1.21", + "@vitejs/plugin-basic-ssl": "2.1.0", + "beasties": "0.3.5", + "browserslist": "^4.26.0", + "esbuild": "0.27.2", + "https-proxy-agent": "7.0.6", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", + "listr2": "9.0.5", + "magic-string": "0.30.21", + "mrmime": "2.0.1", + "parse5-html-rewriting-stream": "8.0.0", + "picomatch": "4.0.3", + "piscina": "5.1.4", + "rolldown": "1.0.0-beta.58", + "sass": "1.97.1", + "semver": "7.7.3", + "source-map-support": "0.5.21", + "tinyglobby": "0.2.15", + "undici": "7.20.0", + "vite": "7.3.0", + "watchpack": "2.5.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "lmdb": "3.4.4" + }, + "peerDependencies": { + "@angular/compiler": "^21.0.0", + "@angular/compiler-cli": "^21.0.0", + "@angular/core": "^21.0.0", + "@angular/localize": "^21.0.0", + "@angular/platform-browser": "^21.0.0", + "@angular/platform-server": "^21.0.0", + "@angular/service-worker": "^21.0.0", + "@angular/ssr": "^21.1.3", + "karma": "^6.4.0", + "less": "^4.2.0", + "ng-packagr": "^21.0.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "tslib": "^2.3.0", + "typescript": ">=5.9 <6.0", + "vitest": "^4.0.8" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + }, + "@angular/localize": { + "optional": true + }, + "@angular/platform-browser": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "karma": { + "optional": true + }, + "less": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@angular/cli": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.1.3.tgz", + "integrity": "sha512-UPtDcpKyrKZRPfym9gTovcibPzl2O/Woy7B8sm45sAnjDH+jDUCcCvuIak7GpH47shQkC2J4yvnHZbD4c6XxcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.2101.3", + "@angular-devkit/core": "21.1.3", + "@angular-devkit/schematics": "21.1.3", + "@inquirer/prompts": "7.10.1", + "@listr2/prompt-adapter-inquirer": "3.0.5", + "@modelcontextprotocol/sdk": "1.26.0", + "@schematics/angular": "21.1.3", + "@yarnpkg/lockfile": "1.1.0", + "algoliasearch": "5.46.2", + "ini": "6.0.0", + "jsonc-parser": "3.3.1", + "listr2": "9.0.5", + "npm-package-arg": "13.0.2", + "pacote": "21.0.4", + "parse5-html-rewriting-stream": "8.0.0", + "resolve": "1.22.11", + "semver": "7.7.3", + "yargs": "18.0.0", + "zod": "4.3.5" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@algolia/abtesting": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.12.2.tgz", + "integrity": "sha512-oWknd6wpfNrmRcH0vzed3UPX0i17o4kYLM5OMITyMVM2xLgaRbIafoxL0e8mcrNNb0iORCJA0evnNDKRYth5WQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@angular/cli/node_modules/@algolia/client-abtesting": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.46.2.tgz", + "integrity": "sha512-oRSUHbylGIuxrlzdPA8FPJuwrLLRavOhAmFGgdAvMcX47XsyM+IOGa9tc7/K5SPvBqn4nhppOCEz7BrzOPWc4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@angular/cli/node_modules/@algolia/client-analytics": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.46.2.tgz", + "integrity": "sha512-EPBN2Oruw0maWOF4OgGPfioTvd+gmiNwx0HmD9IgmlS+l75DatcBkKOPNJN+0z3wBQWUO5oq602ATxIfmTQ8bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@angular/cli/node_modules/@algolia/client-common": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.46.2.tgz", + "integrity": "sha512-Hj8gswSJNKZ0oyd0wWissqyasm+wTz1oIsv5ZmLarzOZAp3vFEda8bpDQ8PUhO+DfkbiLyVnAxsPe4cGzWtqkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@angular/cli/node_modules/@algolia/client-insights": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.46.2.tgz", + "integrity": "sha512-6dBZko2jt8FmQcHCbmNLB0kCV079Mx/DJcySTL3wirgDBUH7xhY1pOuUTLMiGkqM5D8moVZTvTdRKZUJRkrwBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@angular/cli/node_modules/@algolia/client-personalization": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.46.2.tgz", + "integrity": "sha512-1waE2Uqh/PHNeDXGn/PM/WrmYOBiUGSVxAWqiJIj73jqPqvfzZgzdakHscIVaDl6Cp+j5dwjsZ5LCgaUr6DtmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@angular/cli/node_modules/@algolia/client-query-suggestions": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.46.2.tgz", + "integrity": "sha512-EgOzTZkyDcNL6DV0V/24+oBJ+hKo0wNgyrOX/mePBM9bc9huHxIY2352sXmoZ648JXXY2x//V1kropF/Spx83w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@angular/cli/node_modules/@algolia/client-search": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.46.2.tgz", + "integrity": "sha512-ZsOJqu4HOG5BlvIFnMU0YKjQ9ZI6r3C31dg2jk5kMWPSdhJpYL9xa5hEe7aieE+707dXeMI4ej3diy6mXdZpgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@angular/cli/node_modules/@algolia/ingestion": { + "version": "1.46.2", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.46.2.tgz", + "integrity": "sha512-1Uw2OslTWiOFDtt83y0bGiErJYy5MizadV0nHnOoHFWMoDqWW0kQoMFI65pXqRSkVvit5zjXSLik2xMiyQJDWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@angular/cli/node_modules/@algolia/monitoring": { + "version": "1.46.2", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.46.2.tgz", + "integrity": "sha512-xk9f+DPtNcddWN6E7n1hyNNsATBCHIqAvVGG2EAGHJc4AFYL18uM/kMTiOKXE/LKDPyy1JhIerrh9oYb7RBrgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@angular/cli/node_modules/@algolia/recommend": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.46.2.tgz", + "integrity": "sha512-NApbTPj9LxGzNw4dYnZmj2BoXiAc8NmbbH6qBNzQgXklGklt/xldTvu+FACN6ltFsTzoNU6j2mWNlHQTKGC5+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@angular/cli/node_modules/@algolia/requester-browser-xhr": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.46.2.tgz", + "integrity": "sha512-ekotpCwpSp033DIIrsTpYlGUCF6momkgupRV/FA3m62SreTSZUKjgK6VTNyG7TtYfq9YFm/pnh65bATP/ZWJEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@angular/cli/node_modules/@algolia/requester-fetch": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.46.2.tgz", + "integrity": "sha512-gKE+ZFi/6y7saTr34wS0SqYFDcjHW4Wminv8PDZEi0/mE99+hSrbKgJWxo2ztb5eqGirQTgIh1AMVacGGWM1iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@angular/cli/node_modules/@algolia/requester-node-http": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.46.2.tgz", + "integrity": "sha512-ciPihkletp7ttweJ8Zt+GukSVLp2ANJHU+9ttiSxsJZThXc4Y2yJ8HGVWesW5jN1zrsZsezN71KrMx/iZsOYpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@angular/cli/node_modules/algoliasearch": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.46.2.tgz", + "integrity": "sha512-qqAXW9QvKf2tTyhpDA4qXv1IfBwD2eduSW6tUEBFIfCeE9gn9HQ9I5+MaKoenRuHrzk5sQoNh1/iof8mY7uD6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.12.2", + "@algolia/client-abtesting": "5.46.2", + "@algolia/client-analytics": "5.46.2", + "@algolia/client-common": "5.46.2", + "@algolia/client-insights": "5.46.2", + "@algolia/client-personalization": "5.46.2", + "@algolia/client-query-suggestions": "5.46.2", + "@algolia/client-search": "5.46.2", + "@algolia/ingestion": "1.46.2", + "@algolia/monitoring": "1.46.2", + "@algolia/recommend": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@angular/common": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.1.3.tgz", + "integrity": "sha512-Wdbln/UqZM5oVnpfIydRdhhL8A9x3bKZ9Zy1/mM0q+qFSftPvmFZIXhEpFqbDwNYbGUhGzx7t8iULC4sVVp/zA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/core": "21.1.3", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.1.3.tgz", + "integrity": "sha512-gDNLh7MEf7Qf88ktZzS4LJQXCA5U8aQTfK9ak+0mi2ruZ0x4XSjQCro4H6OPKrrbq94+6GcnlSX5+oVIajEY3w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@angular/compiler-cli": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.1.3.tgz", + "integrity": "sha512-nKxoQ89W2B1WdonNQ9kgRnvLNS6DAxDrRHBslsKTlV+kbdv7h59M9PjT4ZZ2sp1M/M8LiofnUfa/s2jd/xYj5w==", + "license": "MIT", + "dependencies": { + "@babel/core": "7.28.5", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^5.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^18.0.0" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/compiler": "21.1.3", + "typescript": ">=5.9 <6.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@angular/core": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.1.3.tgz", + "integrity": "sha512-TbhQxRC7Lb/3WBdm1n8KRsktmVEuGBBp0WRF5mq0Ze4s1YewIM6cULrSw9ACtcL5jdcq7c74ms+uKQsaP/gdcQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/compiler": "21.1.3", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.15.0 || ~0.16.0" + }, + "peerDependenciesMeta": { + "@angular/compiler": { + "optional": true + }, + "zone.js": { + "optional": true + } + } + }, + "node_modules/@angular/forms": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.1.3.tgz", + "integrity": "sha512-YW/YdjM9suZUeJam9agHFXIEE3qQIhGYXMjnnX7xGjOe+CuR2R0qsWn1AR0yrKrNmFspb0lKgM7kTTJyzt8gZg==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/common": "21.1.3", + "@angular/core": "21.1.3", + "@angular/platform-browser": "21.1.3", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/language-service": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-21.1.3.tgz", + "integrity": "sha512-i7iMIMt2rbCDXRuVULbi0I5v4a7ldBgoGdPvHQ17poohTjU4NJ2Jm7p7mUYCGcDlYmWOvgxMGaoiqUs6S5lFPA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@angular/localize": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-21.1.3.tgz", + "integrity": "sha512-o/zFe8t578OP1j9+7iYibkwcE19zVC8xRFl/+f8bLSwqxwqasMNu1/zCa1B2nq8Gd2xwbvX/7kDhAn25yM4FJg==", + "license": "MIT", + "dependencies": { + "@babel/core": "7.28.5", + "@types/babel__core": "7.20.5", + "tinyglobby": "^0.2.12", + "yargs": "^18.0.0" + }, + "bin": { + "localize-extract": "tools/bundles/src/extract/cli.js", + "localize-migrate": "tools/bundles/src/migrate/cli.js", + "localize-translate": "tools/bundles/src/translate/cli.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/compiler": "21.1.3", + "@angular/compiler-cli": "21.1.3" + } + }, + "node_modules/@angular/platform-browser": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.1.3.tgz", + "integrity": "sha512-W+ZMXAioaP7CsACafBCHsIxiiKrRTPOlQ+hcC7XNBwy+bn5mjGONoCgLreQs76M8HNWLtr/OAUAr6h26OguOuA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/animations": "21.1.3", + "@angular/common": "21.1.3", + "@angular/core": "21.1.3" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-21.1.3.tgz", + "integrity": "sha512-wWEjrNtJfxzZmbDWdJSyRau7NWpQ6IFM9QAyn7xH3cQDGCj+Gy9lTU5sUIYQc+7sx3nKWztolc7h/M5meYCTAg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/common": "21.1.3", + "@angular/compiler": "21.1.3", + "@angular/core": "21.1.3", + "@angular/platform-browser": "21.1.3" + } + }, + "node_modules/@angular/router": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.1.3.tgz", + "integrity": "sha512-uAw4LAMHXAPCe4SywhlUEWjMYVbbLHwTxLyduSp1b+9aVwep0juy5O/Xttlxd/oigVe0NMnOyJG9y1Br/ubnrg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/common": "21.1.3", + "@angular/core": "21.1.3", + "@angular/platform-browser": "21.1.3", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/service-worker": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-21.1.3.tgz", + "integrity": "sha512-5qJD2JsFLn9H1BMQvEUzfKZhX7oMkgYRTfuPbQuyuxfR9U8M5EYifRqOtAY6xyIUDjnn0FOS6dqMPOq49DGAog==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "bin": { + "ngsw-config": "ngsw-config.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/core": "21.1.3", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@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/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==", + "license": "MIT" + }, + "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==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-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==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@colsen1991/ngx-translate-extract-marker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@colsen1991/ngx-translate-extract-marker/-/ngx-translate-extract-marker-3.0.1.tgz", + "integrity": "sha512-faQeH8zTKB8U+7kdsL54Mj48uJuMKhZ0/t2qONrxyBE5GwWGWxTf778cBi4UR23nOmBzo8A2fyEXCY0gtdkOVQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=14.2.0", + "@angular/core": ">=14.2.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "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.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "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.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@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/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/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "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/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "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/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", + "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.5.tgz", + "integrity": "sha512-WELs+hj6xcilkloBXYf9XXK8tYEnKsgLj01Xl5ONUJpKjmT5hGVUzNUS5tooUxs7pGMrw+jFD/41WpqW4V3LDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 8", + "listr2": "9.0.5" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.4.4.tgz", + "integrity": "sha512-XaKL705gDWd6XVls3ATDj13ZdML/LqSIxwgnYpG8xTzH2ifArx8fMMDdvqGE/Emd+W6R90W2fveZcJ0AyS8Y0w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.4.4.tgz", + "integrity": "sha512-GPHGEVcwJlkD01GmIr7B4kvbIcUDS2+kBadVEd7lU4can1RZaZQLDDBJRrrNfS2Kavvl0VLI/cMv7UASAXGrww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.4.4.tgz", + "integrity": "sha512-cmev5/dZr5ACKri9f6GU6lZCXTjMhV72xujlbOhFCgFXrt4W0TxGsmY8kA1BITvH60JBKE50cSxsiulybAbrrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.4.4.tgz", + "integrity": "sha512-mALqr7DE42HsiwVTKpQWxacjHoJk+e9p00RWIJqTACh/hpucxp/0lK/XMh5XzWnU/TDCZLukq1+vNqnNumTP/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.4.4.tgz", + "integrity": "sha512-QjLs8OcmCNcraAcLoZyFlo0atzBJniQLLwhtR+ymQqS5kLYpV5RqwriL87BW+ZiR9ZiGgZx3evrz5vnWPtJ1fQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-arm64": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.4.4.tgz", + "integrity": "sha512-tr/pwHDlZ33forLGAr0tI04cRmP4SgF93yHbb+2zvZiDEyln5yMHhbKDySxY66aUOkhvBvTuHq9q/3YmTj6ZHQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.4.4.tgz", + "integrity": "sha512-KRzfocJzB/mgoTCqnMawuLSKheHRVTqWfSmouIgYpFs6Hx4zvZSvsZKSCEb5gHmICy7qsx9l06jk3MFTtiFVAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@napi-rs/nice": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz", + "integrity": "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.1.1", + "@napi-rs/nice-android-arm64": "1.1.1", + "@napi-rs/nice-darwin-arm64": "1.1.1", + "@napi-rs/nice-darwin-x64": "1.1.1", + "@napi-rs/nice-freebsd-x64": "1.1.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1", + "@napi-rs/nice-linux-arm64-gnu": "1.1.1", + "@napi-rs/nice-linux-arm64-musl": "1.1.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.1.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.1.1", + "@napi-rs/nice-linux-s390x-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-musl": "1.1.1", + "@napi-rs/nice-openharmony-arm64": "1.1.1", + "@napi-rs/nice-win32-arm64-msvc": "1.1.1", + "@napi-rs/nice-win32-ia32-msvc": "1.1.1", + "@napi-rs/nice-win32-x64-msvc": "1.1.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz", + "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz", + "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz", + "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz", + "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz", + "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz", + "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz", + "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz", + "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz", + "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz", + "integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz", + "integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz", + "integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz", + "integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-openharmony-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz", + "integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz", + "integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz", + "integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz", + "integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@ngneat/overview": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@ngneat/overview/-/overview-7.0.0.tgz", + "integrity": "sha512-nWSyoWHYGTyE6miNQ1AV2Or9yB+JcEwfQlCPhigq39HUd/Y40J28dAslIN2FCpAONBu2hm4XpYMb+Ya1DUgR2g==", + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/core": ">=20" + } + }, + "node_modules/@ngx-translate/core": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-16.0.4.tgz", + "integrity": "sha512-s8llTL2SJvROhqttxvEs7Cg+6qSf4kvZPFYO+cTOY1d8DWTjlutRkWAleZcPPoeX927Dm7ALfL07G7oYDJ7z6w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=16", + "@angular/core": ">=16" + } + }, + "node_modules/@ngx-translate/http-loader": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-16.0.1.tgz", + "integrity": "sha512-xJEOUpvs6Zfc8G4cmQmegFOEpfYSoplTHHoisPNrATXjRBjpaKsBaPOXlZsuFUW2XV00s16gIyI4+9z1XkO5bw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=16", + "@angular/core": ">=16" + } + }, + "node_modules/@ngxpert/hot-toast": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@ngxpert/hot-toast/-/hot-toast-6.1.0.tgz", + "integrity": "sha512-eD0kYsoi7/lkdxnHuBfdAM0NeInMAjWQHHJgKlkInGn60tmcGjFGWow7l/ZPOdZEjKq4ARhDsA7yDrXMG4J+UQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">= 21.0.0", + "@angular/core": ">= 21.0.0", + "@ngneat/overview": ">= 7.0.0" + } + }, + "node_modules/@npmcli/agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", + "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/fs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.1.tgz", + "integrity": "sha512-+XTFxK2jJF/EJJ5SoAzXk3qwIDfvFc5/g+bD274LZ7uY7LE8sTfG6Z8rOanPl2ZEvZWqNvmEdtXC25cE54VcoA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^9.0.0", + "ini": "^6.0.0", + "lru-cache": "^11.2.1", + "npm-pick-manifest": "^11.0.1", + "proc-log": "^6.0.0", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.4.tgz", + "integrity": "sha512-jCErc4h4RnTPjFq53G4whhjAMbUAqinGrCrTT4dmMNyi4zTthK+wphqbRLJtL4BN/Mq7Zzltr0m/b1X0m7PGFQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", + "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-4.0.0.tgz", + "integrity": "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^5.0.0", + "npm-normalize-package-bin": "^5.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-5.0.0.tgz", + "integrity": "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.4.tgz", + "integrity": "sha512-0wInJG3j/K40OJt/33ax47WfWMzZTm6OQxB9cDhTt5huCP2a9g2GnlsxmfN+PulItNPIpPrZ+kfwwUil7eHcZQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "glob": "^13.0.0", + "hosted-git-info": "^9.0.0", + "json-parse-even-better-errors": "^5.0.0", + "proc-log": "^6.0.0", + "semver": "^7.5.3", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/glob": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz", + "integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.2", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/package-json/node_modules/minimatch": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", + "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.4.tgz", + "integrity": "sha512-jCErc4h4RnTPjFq53G4whhjAMbUAqinGrCrTT4dmMNyi4zTthK+wphqbRLJtL4BN/Mq7Zzltr0m/b1X0m7PGFQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", + "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", + "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.3.tgz", + "integrity": "sha512-ER2N6itRkzWbbtVmZ9WKaWxVlKlOeBFF1/7xx+KA5J1xKa4JjUwBdb6tDpk0v1qA+d+VDwHI9qmLcXSWcmi+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "node-gyp": "^12.1.0", + "proc-log": "^6.0.0", + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/isexe": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.4.tgz", + "integrity": "sha512-jCErc4h4RnTPjFq53G4whhjAMbUAqinGrCrTT4dmMNyi4zTthK+wphqbRLJtL4BN/Mq7Zzltr0m/b1X0m7PGFQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@npmcli/run-script/node_modules/which": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", + "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.106.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.106.0.tgz", + "integrity": "sha512-QdsH3rZq480VnOHSHgPYOhjL8O8LBdcnSjM408BpPCCUc0JYYZPG9Gafl9i3OcGk/7137o+gweb4cCv3WAUykg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.58.tgz", + "integrity": "sha512-mWj5eE4Qc8TbPdGGaaLvBb9XfDPvE1EmZkJQgiGKwchkWH4oAJcRAKMTw7ZHnb1L+t7Ah41sBkAecaIsuUgsug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.58.tgz", + "integrity": "sha512-wFxUymI/5R8bH8qZFYDfAxAN9CyISEIYke+95oZPiv6EWo88aa5rskjVcCpKA532R+klFmdqjbbaD56GNmTF4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.58.tgz", + "integrity": "sha512-ybp3MkPj23VDV9PhtRwdU5qrGhlViWRV5BjKwO6epaSlUD5lW0WyY+roN3ZAzbma/9RrMTgZ/a/gtQq8YXOcqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.58.tgz", + "integrity": "sha512-Evxj3yh7FWvyklUYZa0qTVT9N2zX9TPDqGF056hl8hlCZ9/ndQ2xMv6uw9PD1VlLpukbsqL+/C6M0qwipL0QMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.58.tgz", + "integrity": "sha512-tYeXprDOrEgVHUbPXH6MPso4cM/c6RTkmJNICMQlYdki4hGMh92aj3yU6CKs+4X5gfG0yj5kVUw/L4M685SYag==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.58.tgz", + "integrity": "sha512-N78vmZzP6zG967Ohr+MasCjmKtis0geZ1SOVmxrA0/bklTQSzH5kHEjW5Qn+i1taFno6GEre1E40v0wuWsNOQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.58.tgz", + "integrity": "sha512-l+p4QVtG72C7wI2SIkNQw/KQtSjuYwS3rV6AKcWrRBF62ClsFUcif5vLaZIEbPrCXu5OFRXigXFJnxYsVVZqdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.58.tgz", + "integrity": "sha512-urzJX0HrXxIh0FfxwWRjfPCMeInU9qsImLQxHBgLp5ivji1EEUnOfux8KxPPnRQthJyneBrN2LeqUix9DYrNaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.58.tgz", + "integrity": "sha512-7ijfVK3GISnXIwq/1FZo+KyAUJjL3kWPJ7rViAL6MWeEBhEgRzJ0yEd9I8N9aut8Y8ab+EKFJyRNMWZuUBwQ0A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.58.tgz", + "integrity": "sha512-/m7sKZCS+cUULbzyJTIlv8JbjNohxbpAOA6cM+lgWgqVzPee3U6jpwydrib328JFN/gF9A99IZEnuGYqEDJdww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.58.tgz", + "integrity": "sha512-6SZk7zMgv+y3wFFQ9qE5P9NnRHcRsptL1ypmudD26PDY+PvFCvfHRkJNfclWnvacVGxjowr7JOL3a9fd1wWhUw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.58.tgz", + "integrity": "sha512-sFqfYPnBZ6xBhMkadB7UD0yjEDRvs7ipR3nCggblN+N4ODCXY6qhg/bKL39+W+dgQybL7ErD4EGERVbW9DAWvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.58.tgz", + "integrity": "sha512-AnFWJdAqB8+IDPcGrATYs67Kik/6tnndNJV2jGRmwlbeNiQQ8GhRJU8ETRlINfII0pqi9k4WWLnb00p1QCxw/Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.58.tgz", + "integrity": "sha512-qWhDs6yFGR5xDfdrwiSa3CWGIHxD597uGE/A9xGqytBjANvh4rLCTTkq7szhMV4+Ygh+PMS90KVJ8xWG/TkX4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@schematics/angular": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.1.3.tgz", + "integrity": "sha512-obJvWBhzRdsYL2msM4+8bQD21vFl3VxaVsuiq6iIfYsxhU5i2Iar2wM9NaRaIIqAYhZ8ehQQ/moB9BEbWvDCTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "21.1.3", + "@angular-devkit/schematics": "21.1.3", + "jsonc-parser": "3.3.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@sigstore/bundle": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz", + "integrity": "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/core": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.1.0.tgz", + "integrity": "sha512-o5cw1QYhNQ9IroioJxpzexmPjfCe7gzafd2RY3qnMpxr4ZEja+Jad/U8sgFpaue6bOaF+z7RVkyKVV44FN+N8A==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.0.tgz", + "integrity": "sha512-MM8XIwUjN2bwvCg1QvrMtbBmpcSHrkhFSCu1D11NyPvDQ25HEc4oG5/OcQfd/Tlf/OxmKWERDj0zGE23jQaMwA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.0.tgz", + "integrity": "sha512-Vx1RmLxLGnSUqx/o5/VsCjkuN5L7y+vxEEwawvc7u+6WtX2W4GNa7b9HEjmcRWohw/d6BpATXmvOwc78m+Swdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.1.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^15.0.3", + "proc-log": "^6.1.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.1.tgz", + "integrity": "sha512-OPZBg8y5Vc9yZjmWCHrlWPMBqW5yd8+wFNl+thMdtcWz3vjVSoJQutF8YkrzI0SLGnkuFof4HSsWUhXrf219Lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^4.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.0.tgz", + "integrity": "sha512-mNe0Iigql08YupSOGv197YdHpPPr+EzDZmfCgMc7RPNaZTw5aLN01nBl6CHJOh3BGtnMIj83EeN4butBchc8Ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.1.0", + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.1.0.tgz", + "integrity": "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^10.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@tufjs/models/node_modules/minimatch": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jasmine": { + "version": "5.1.15", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.15.tgz", + "integrity": "sha512-ZAC8KjmV2MJxbNTrwXFN+HKeajpXQZp6KpPiR6Aa4XvaEnjP6qh23lL/Rqb7AYzlp3h/rcwDrQ7Gg7q28cQTQg==", + "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": "25.2.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.2.tgz", + "integrity": "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", + "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/type-utils": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.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.54.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", + "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "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", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", + "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.54.0", + "@typescript-eslint/types": "^8.54.0", + "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.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", + "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", + "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", + "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.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", + "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", + "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", + "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.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", + "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.54.0", + "@typescript-eslint/tsconfig-utils": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", + "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", + "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/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/@vitejs/plugin-basic-ssl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.0.tgz", + "integrity": "sha512-dOxxrhgyDIEUADhb/8OlV9JIqYLgos03YorAueTIeOUskLJSEsfwCByjbu98ctXitUN3znXKp0bYD/WHSudCeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0" + } + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/abs": { + "version": "1.3.15", + "resolved": "https://registry.npmjs.org/abs/-/abs-1.3.15.tgz", + "integrity": "sha512-bpFChpVyZ2F2ppgx7qjZ5TTEO6VVwBauUZDZibpclRGhfcXTHyj11nlqwrg5dN1knxCchssROehm76uCcCayRA==", + "license": "MIT", + "dependencies": { + "ul": "^5.0.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/algoliasearch": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.48.0.tgz", + "integrity": "sha512-aD8EQC6KEman6/S79FtPdQmB7D4af/etcRL/KwiKFKgAE62iU8c5PeEQvpvIcBPurC3O/4Lj78nOl7ZcoazqSw==", + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.14.0", + "@algolia/client-abtesting": "5.48.0", + "@algolia/client-analytics": "5.48.0", + "@algolia/client-common": "5.48.0", + "@algolia/client-insights": "5.48.0", + "@algolia/client-personalization": "5.48.0", + "@algolia/client-query-suggestions": "5.48.0", + "@algolia/client-search": "5.48.0", + "@algolia/ingestion": "1.48.0", + "@algolia/monitoring": "1.48.0", + "@algolia/recommend": "5.48.0", + "@algolia/requester-browser-xhr": "5.48.0", + "@algolia/requester-fetch": "5.48.0", + "@algolia/requester-node-http": "5.48.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/angular-eslint": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/angular-eslint/-/angular-eslint-21.2.0.tgz", + "integrity": "sha512-pERqqHIMwD34UT0FoHSNTt4V332vHiAzgkY0rgdUaqSamS94IzbF02EfFxygr53UogQQOXhpLbSSDMOyovB3TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": ">= 21.0.0 < 22.0.0", + "@angular-devkit/schematics": ">= 21.0.0 < 22.0.0", + "@angular-eslint/builder": "21.2.0", + "@angular-eslint/eslint-plugin": "21.2.0", + "@angular-eslint/eslint-plugin-template": "21.2.0", + "@angular-eslint/schematics": "21.2.0", + "@angular-eslint/template-parser": "21.2.0", + "@typescript-eslint/types": "^8.0.0", + "@typescript-eslint/utils": "^8.0.0" + }, + "peerDependencies": { + "@angular/cli": ">= 21.0.0 < 22.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*", + "typescript-eslint": "^8.0.0" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/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/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/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/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "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/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/beasties": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.3.5.tgz", + "integrity": "sha512-NaWu+f4YrJxEttJSm16AzMIFtVldCvaJ68b1L098KpqXmxt9xOLtKoLkKxb8ekhOrLqEJAbvT6n6SEvB/sac7A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "css-select": "^6.0.0", + "css-what": "^7.0.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "htmlparser2": "^10.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.49", + "postcss-media-query-parser": "^0.2.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "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/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/bootstrap": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", + "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/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.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "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.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz", + "integrity": "sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^5.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^13.0.0", + "unique-filename": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz", + "integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.2", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "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/capture-stack-trace": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.2.tgz", + "integrity": "sha512-X/WM2UQs6VMHUtjUDnZTRI+i1crWteJySFzr9UpGoQa4WQffXVTTXuekjl7TjZRlcF2XfjgITT0HxZ9RnxeT0w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", + "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^7.1.0", + "string-width": "^8.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/connect/node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/connect/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/connect/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/create-error-class": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", + "integrity": "sha512-gYTKKexFO3kh200H1Nit76sRwRtOY32vQd3jpAQKpLtZqyNsSQNfI4N7o3eP2wUjV35pTWKRYqFUDBvUha/Pkw==", + "license": "MIT", + "dependencies": { + "capture-stack-trace": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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-select": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", + "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^7.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "nth-check": "^2.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", + "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "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/deffy": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/deffy/-/deffy-2.2.5.tgz", + "integrity": "sha512-6TX2cfIo97eKqWmqgMDAUulCwnveAe3K+4VGsTGPJsL3NtSEnSBFZ3sUXdS4EBhZ8GbdaZBzXQ04ton18dJrug==", + "license": "MIT", + "dependencies": { + "typpy": "^2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine.io": { + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz", + "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz", + "integrity": "sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "punycode": "^1.4.1", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/err/-/err-1.1.1.tgz", + "integrity": "sha512-N97Ybd2jJHVQ+Ft3Q5+C2gM3kgygkdeQmEqbN2z15UTVyyEsIwLA1VK39O1DHEJhXbwIFcJLqm6iARNhFANcQA==", + "license": "MIT", + "dependencies": { + "typpy": "^2.2.0" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "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.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@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.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.0.tgz", + "integrity": "sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "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/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/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/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/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/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "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/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/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/exec-limiter": { + "version": "3.2.14", + "resolved": "https://registry.npmjs.org/exec-limiter/-/exec-limiter-3.2.14.tgz", + "integrity": "sha512-ZQjJmAnXD+1kQ6ejMZAS5Vxdt7LLMz0Eq7mEu6+7NhlauykuyLihhUkpp4S784QKsmJQIpuuERhQ8Tav8bF3zQ==", + "license": "MIT", + "dependencies": { + "limit-it": "^3.0.0", + "typpy": "^2.1.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-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/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/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/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.name": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/function.name/-/function.name-1.0.14.tgz", + "integrity": "sha512-s99L814NRuLxwF2sJMIcLhkQhueGXb3oKyvorzrUKKwlVB0SBbWrgZt4+EwKAo3ujCXnT7vshmCvXgZA09kCMw==", + "license": "MIT", + "dependencies": { + "noop6": "^1.0.1" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/git-package-json": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/git-package-json/-/git-package-json-1.4.11.tgz", + "integrity": "sha512-A/P5K2qqQ52+BwBf+qyrjtdauMlb7n1WVa++/VPDxTcgKZ2X5/Eh/EQwbxNvRKBsKAkMAeyV/UIdnb/saVFnnQ==", + "license": "MIT", + "dependencies": { + "deffy": "^2.2.1", + "err": "^1.1.1", + "gry": "^5.0.0", + "normalize-package-data": "^2.3.5", + "oargv": "^3.4.1", + "one-by-one": "^3.1.0", + "r-json": "^1.2.1", + "r-package-json": "^1.0.0", + "tmp": "0.0.28" + } + }, + "node_modules/git-package-json/node_modules/tmp": { + "version": "0.0.28", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.28.tgz", + "integrity": "sha512-c2mmfiBmND6SOVxzogm1oda0OJ1HZVIk/5n26N59dDTh80MUeavpiCls4PGAdkX1PFkKokLpcf7prSjCeXLsJg==", + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.1" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/git-source": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/git-source/-/git-source-1.1.11.tgz", + "integrity": "sha512-oubUf/uply9xvR5olZxxPpip19wMEpESN3bFfPcFMvl/0fwrVrcAppwOJ7Dghcguze68WAIjs/A1YrdMDIW8XA==", + "license": "MIT", + "dependencies": { + "git-url-parse": "^5.0.1" + } + }, + "node_modules/git-up": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/git-up/-/git-up-1.2.1.tgz", + "integrity": "sha512-SRVN3rOLACva8imc7BFrB6ts5iISWKH1/h/1Z+JZYoUI7UVQM7gQqk4M2yxUENbq2jUUT09NEND5xwP1i7Ktlw==", + "license": "MIT", + "dependencies": { + "is-ssh": "^1.0.0", + "parse-url": "^1.0.0" + } + }, + "node_modules/git-url-parse": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-5.0.1.tgz", + "integrity": "sha512-4uSiOgrryNEMBX+gTWogenYRUh2j1D+95STTSEF2RCTgLkfJikl8c7BGr0Bn274hwuxTsbS2/FQ5pVS9FoXegQ==", + "license": "MIT", + "dependencies": { + "git-up": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-5.6.0.tgz", + "integrity": "sha512-MnypzkaW8dldA8AbJFjMs7y14+ykd2V8JCLKSvX1Gmzx1alH3Y+3LArywHDoAF2wS3pnZp4gacoYtvqBeF6drQ==", + "license": "MIT", + "dependencies": { + "create-error-class": "^3.0.1", + "duplexer2": "^0.1.4", + "is-plain-obj": "^1.0.0", + "is-redirect": "^1.0.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "lowercase-keys": "^1.0.0", + "node-status-codes": "^1.0.0", + "object-assign": "^4.0.1", + "parse-json": "^2.1.0", + "pinkie-promise": "^2.0.0", + "read-all-stream": "^3.0.0", + "readable-stream": "^2.0.5", + "timed-out": "^2.0.0", + "unzip-response": "^1.0.0", + "url-parse-lax": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/gry": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/gry/-/gry-5.0.8.tgz", + "integrity": "sha512-meq9ZjYVpLzZh3ojhTg7IMad9grGsx6rUUKHLqPnhLXzJkRQvEL2U3tQpS5/WentYTtHtxkT3Ew/mb10D6F6/g==", + "license": "MIT", + "dependencies": { + "abs": "^1.2.1", + "exec-limiter": "^3.0.0", + "one-by-one": "^3.0.0", + "ul": "^5.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/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", + "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/hosted-git-info": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", + "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/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/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", + "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/immutable": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", + "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "dev": true, + "license": "MIT" + }, + "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/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "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.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "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-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha512-cr/SlUEe5zOGmzvj9bUyC4LVvkNVAXu4GytXLNMr1pny+a65MpQ9IJzFHD5vi7FyJgb4qt27+eS3TuQnqB+RQw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-retry-allowed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-ssh": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.1.tgz", + "integrity": "sha512-JNeu1wQsHjyHgn9NcWTaXq6zWSR6hqE0++zhfZlkFBbScNkyvxCdeV8sRkSBaeLKxmbpR21brail63ACNxJ0Tg==", + "license": "MIT", + "dependencies": { + "protocols": "^2.0.1" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "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/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterate-object": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/iterate-object/-/iterate-object-1.3.5.tgz", + "integrity": "sha512-eL23u8oFooYTq6TtJKjp2RYjZnCkUYQvC0T/6fJfWykXJ3quvdDdzKZ3CEjy8b3JGOvLTjDYMEMIp5243R906A==", + "license": "MIT" + }, + "node_modules/jasmine-core": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.2.0.tgz", + "integrity": "sha512-tSAtdrvWybZkQmmaIoDgnvHG8ORUNw5kEVlO5CvrXj02Jjr9TZrmjFq7FUiOUzJiOP2wLGYT6PgrQgQF4R1xiw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "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.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "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-parse-even-better-errors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", + "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "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==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/karma": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", + "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.7.2", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-chrome-launcher": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz", + "integrity": "sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "which": "^1.2.1" + } + }, + "node_modules/karma-chrome-launcher/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/karma-coverage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", + "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/karma-coverage/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma-coverage/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma-coverage/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma-coverage/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/karma-jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", + "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jasmine-core": "^4.1.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "karma": "^6.0.0" + } + }, + "node_modules/karma-jasmine-html-reporter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.1.0.tgz", + "integrity": "sha512-sPQE1+nlsn6Hwb5t+HHwyy0A1FNCVKuL1192b+XNauMYWThz2kweiBVW1DqloRpVvZIJkIoHVB7XRpK78n1xbQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "jasmine-core": "^4.0.0 || ^5.0.0", + "karma": "^6.0.0", + "karma-jasmine": "^5.0.0" + } + }, + "node_modules/karma-jasmine/node_modules/jasmine-core": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.1.tgz", + "integrity": "sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/karma/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/karma/node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/karma/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma/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/karma/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/karma/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/karma/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/karma/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/karma/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/karma/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/karma/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/karma/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/karma/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/karma/node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/karma/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/karma/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/karma/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/karma/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/karma/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "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/limit-it": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/limit-it/-/limit-it-3.2.11.tgz", + "integrity": "sha512-VdLa1lZYZnzT98oLMeCDl6Lwd9cEYIMQlPg34qL6CYuA+yQKoG7K12tfgI5K6bRC51kRM8v1UX67IhpNsnvo3A==", + "license": "MIT", + "dependencies": { + "typpy": "^2.0.0" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lmdb": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.4.4.tgz", + "integrity": "sha512-+Y2DqovevLkb6DrSQ6SXTYLEd6kvlRbhsxzgJrk7BUfOVA/mt21ak6pFDZDKxiAczHMWxrb02kXBTSTIA0O94A==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "msgpackr": "^1.11.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.5.3", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.4.4", + "@lmdb/lmdb-darwin-x64": "3.4.4", + "@lmdb/lmdb-linux-arm": "3.4.4", + "@lmdb/lmdb-linux-arm64": "3.4.4", + "@lmdb/lmdb-linux-x64": "3.4.4", + "@lmdb/lmdb-win32-arm64": "3.4.4", + "@lmdb/lmdb-win32-x64": "3.4.4" + } + }, + "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": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "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/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-fetch-happen": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.3.tgz", + "integrity": "sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^6.0.0", + "promise-retry": "^2.0.1", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.1.tgz", + "integrity": "sha512-yHK8pb0iCGat0lDrs/D6RZmCdaBT64tULXjdxjSMAqoDi18Q3qKEUTHypHQZQd9+FYpIS+lkvpq6C/R6SbUeRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^2.0.0", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", + "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/msgpackr": { + "version": "1.11.8", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.8.tgz", + "integrity": "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA==", + "dev": true, + "license": "MIT", + "optional": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-gyp": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.2.0.tgz", + "integrity": "sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^15.0.0", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.4.tgz", + "integrity": "sha512-jCErc4h4RnTPjFq53G4whhjAMbUAqinGrCrTT4dmMNyi4zTthK+wphqbRLJtL4BN/Mq7Zzltr0m/b1X0m7PGFQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", + "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "license": "MIT" + }, + "node_modules/node-status-codes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-status-codes/-/node-status-codes-1.0.0.tgz", + "integrity": "sha512-1cBMgRxdMWE8KeWCqk2RIOrvUb0XCwYfEsY5/y2NlXyq4Y/RumnOZvTj4Nbr77+Vb2C+kyBoRTdkNOS8L3d/aQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/noop6": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/noop6/-/noop6-1.0.10.tgz", + "integrity": "sha512-WZvuCILZFZHK+WuqCQwxLBGllkBK1ct8s8Mu9FMDbEsBE6/bqNxyFGbX7Xky+6bYFL8X2Ou4Cis4CJyrwXLvQA==", + "license": "MIT" + }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "license": "ISC" + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "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-bundled": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", + "integrity": "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-install-checks": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-8.0.0.tgz", + "integrity": "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", + "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-package-arg": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz", + "integrity": "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-packlist": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.3.tgz", + "integrity": "sha512-zPukTwJMOu5X5uvm0fztwS5Zxyvmk38H/LfidkOMt3gbZVCyro2cD/ETzwzVPcWZA3JOyPznfUN/nkyFiyUbxg==", + "dev": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^8.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz", + "integrity": "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^8.0.0", + "npm-normalize-package-bin": "^5.0.0", + "npm-package-arg": "^13.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.1.tgz", + "integrity": "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^4.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^15.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^13.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/oargv": { + "version": "3.4.11", + "resolved": "https://registry.npmjs.org/oargv/-/oargv-3.4.11.tgz", + "integrity": "sha512-FGTon9C71936EnOjx/NTsMxlLeWmw8zQQld4KDmgRxRtZ8fH1XpbLLRHmOioeZs/WoURz2OGR4KmDoTaL4ErJQ==", + "license": "MIT", + "dependencies": { + "iterate-object": "^1.1.0", + "ul": "^5.0.0" + } + }, + "node_modules/obj-def": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/obj-def/-/obj-def-1.0.10.tgz", + "integrity": "sha512-RJpNUkO+1r/rXTBs82iU4scoC9Q1yp9HZbSk0ldpFe8362S6eTjUjSgTmECa1TtOBIe5pn4pwSzxIiWc8+jmWg==", + "license": "MIT", + "dependencies": { + "deffy": "^2.2.2" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-by-one": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/one-by-one/-/one-by-one-3.2.9.tgz", + "integrity": "sha512-H10TAq02LKrkSRTQz1mgvcKb64rRajZ+B5HWHBvkGigYNCPqL0Q/tLIN3vfha/DqZxXeKNfyCmgfEYo2hgFQgA==", + "license": "MIT", + "dependencies": { + "obj-def": "^1.0.0", + "sliced": "^1.0.1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.0.0.tgz", + "integrity": "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.2.2", + "string-width": "^8.1.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ordered-binary": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.1.tgz", + "integrity": "sha512-QkCdPooczexPLiXIrbVOPYkR3VO3T6v2OyKRkR1Xbhpy7/LAVXwahnRCgRp78Oe/Ehf0C/HATAxfSr6eA1oX+w==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "license": "MIT", + "engines": { + "node": ">=0.10.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/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-2.4.0.tgz", + "integrity": "sha512-PRg65iXMTt/uK8Rfh5zvzkUbfAPitF17YaCY+IbHsYgksiLvtzWWTUildHth3mVaZ7871OJ7gtP4LBRBlmAdXg==", + "license": "MIT", + "dependencies": { + "got": "^5.0.0", + "registry-auth-token": "^3.0.1", + "registry-url": "^3.0.3", + "semver": "^5.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/package-json-path": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/package-json-path/-/package-json-path-1.0.10.tgz", + "integrity": "sha512-DOlmVIfx+qDHHWaaxg573brZ8mH0Nxo4ecYA4SKkrpCOhCP64NXk7VxJtWVKZQ9urfU2Ivl74HeYUO42PLCpLw==", + "license": "MIT", + "dependencies": { + "abs": "^1.2.1" + } + }, + "node_modules/package-json/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/package.json": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/package.json/-/package.json-2.0.1.tgz", + "integrity": "sha512-pSxZ6XR5yEawRN2ekxx9IKgPN5uNAYco7MCPxtBEWMKO3UKWa1X2CtQMzMgloeGj2g2o6cue3Sb5iPkByIJqlw==", + "deprecated": "Use pkg.json instead.", + "license": "MIT", + "dependencies": { + "git-package-json": "^1.4.0", + "git-source": "^1.1.0", + "package-json": "^2.3.1" + } + }, + "node_modules/pacote": { + "version": "21.0.4", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.0.4.tgz", + "integrity": "sha512-RplP/pDW0NNNDh3pnaoIWYPvNenS7UqMbXyvMqJczosiFWTeGGwJC2NQBLqKf4rGLFfwCOnntw1aEp9Jiqm1MA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "@npmcli/installed-package-contents": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "@npmcli/run-script": "^10.0.0", + "cacache": "^20.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^13.0.0", + "npm-packlist": "^10.0.1", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^6.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^4.0.0", + "ssri": "^13.0.0", + "tar": "^7.4.3" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "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/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "license": "MIT", + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-url": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-1.3.11.tgz", + "integrity": "sha512-1wj9nkgH/5EboDxLwaTMGJh3oH3f+Gue+aGdh631oCqoSBpokzmMmOldvOeBPtB8GJBYJbaF93KPzlkU+Y1ksg==", + "license": "MIT", + "dependencies": { + "is-ssh": "^1.3.0", + "protocols": "^1.4.0" + } + }, + "node_modules/parse-url/node_modules/protocols": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-1.4.8.tgz", + "integrity": "sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg==", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-8.0.0.tgz", + "integrity": "sha512-wzh11mj8KKkno1pZEu+l2EVeWsuKDfR5KNWZOTsslfUX8lPDZx77m9T0kIoAVkFtD1nx6YF8oh4BnPHvxMtNMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0", + "parse5": "^8.0.0", + "parse5-sax-parser": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-8.0.0.tgz", + "integrity": "sha512-/dQ8UzHZwnrzs3EvDj6IkKrD/jIZyTlB+8XrHJvcjNgRdmWruNdN9i9RK/JtxakmlUdPwKubKPTCqvbTgzGhrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "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==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "license": "MIT", + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/piscina": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.1.4.tgz", + "integrity": "sha512-7uU4ZnKeQq22t9AsmHGD2w4OYQGonwFnTypDypaWi7Qr2EvQIFVtG8J5D/3bE7W123Wdc9+v4CZDu5hJXVCtBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.x" + }, + "optionalDependencies": { + "@napi-rs/nice": "^1.0.4" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "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/prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/protocols": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.2.tgz", + "integrity": "sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/r-json": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/r-json/-/r-json-1.3.1.tgz", + "integrity": "sha512-5nhRFfjVMQdrwKUfUlRpDUCocdKtjSnYZ1R/86mpZDV3MfsZ3dYYNjSGuMX+mPBvFvQBhdzxSqxkuLPLv4uFGg==", + "license": "MIT", + "dependencies": { + "w-json": "1.3.10" + } + }, + "node_modules/r-package-json": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/r-package-json/-/r-package-json-1.0.10.tgz", + "integrity": "sha512-g+KLu+aq3tkhW6gzjsfdWAyd+ZkueLTzkX2zpB2GIW7M/lOXal3nB8U36XOrIBGogJsz2H//xWA4mj9uGlcigw==", + "license": "MIT", + "dependencies": { + "package-json-path": "^1.0.0", + "r-json": "^1.2.1" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-all-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", + "integrity": "sha512-DI1drPHbmBcUDWrJ7ull/F2Qb8HkwBncVx8/RpKYFSIACYaVRQReISYPdZz/mt1y1+qMCOrfReTopERmaxtP6w==", + "license": "MIT", + "dependencies": { + "pinkie-promise": "^2.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/registry-auth-token": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", + "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", + "license": "MIT", + "dependencies": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", + "license": "MIT", + "dependencies": { + "rc": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "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/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.58.tgz", + "integrity": "sha512-v1FCjMZCan7f+xGAHBi+mqiE4MlH7I+SXEHSQSJoMOGNNB2UYtvMiejsq9YuUOiZjNeUeV/a21nSFbrUR+4ZCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.106.0", + "@rolldown/pluginutils": "1.0.0-beta.58" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-beta.58", + "@rolldown/binding-darwin-arm64": "1.0.0-beta.58", + "@rolldown/binding-darwin-x64": "1.0.0-beta.58", + "@rolldown/binding-freebsd-x64": "1.0.0-beta.58", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.58", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.58", + "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.58", + "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.58", + "@rolldown/binding-linux-x64-musl": "1.0.0-beta.58", + "@rolldown/binding-openharmony-arm64": "1.0.0-beta.58", + "@rolldown/binding-wasm32-wasi": "1.0.0-beta.58", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.58", + "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.58" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize.css": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz", + "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==", + "license": "CC0-1.0" + }, + "node_modules/sass": { + "version": "1.97.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.1.tgz", + "integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "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/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/sigstore": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.0.tgz", + "integrity": "sha512-/fUgUhYghuLzVT/gaJoeVehLCgZiUxPCPMcyVNY0lIf/cTCz58K/WTI7PefDarXxp9nUKpEwg1yyz3eSBMTtgA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.1.0", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^4.1.0", + "@sigstore/tuf": "^4.0.1", + "@sigstore/verify": "^3.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/sliced": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", + "integrity": "sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==", + "license": "MIT" + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.18.3" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "license": "CC0-1.0" + }, + "node_modules/ssri": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.0.tgz", + "integrity": "sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.1.tgz", + "integrity": "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/timed-out": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-2.0.0.tgz", + "integrity": "sha512-pqqJOi1rF5zNs/ps4vmbE4SFCrM4iR7LW+GHAsHqO/EumqbIWceioevYLM5xZRgQSH6gFgL9J/uB7EcJhQ9niQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "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/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tuf-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.1.0.tgz", + "integrity": "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "4.1.0", + "debug": "^4.4.3", + "make-fetch-happen": "^15.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.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-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz", + "integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.54.0", + "@typescript-eslint/parser": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/typpy": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/typpy/-/typpy-2.4.0.tgz", + "integrity": "sha512-a16Uv5doNtvHzaG4wZCHmXN+l9xxmTMpyODtPz7B3DSTsDVNXilTSJGuNw68sUh0Un4bf+ghRMbEcJCI6r06mQ==", + "license": "MIT", + "dependencies": { + "function.name": "^1.0.3" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.41", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz", + "integrity": "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ul": { + "version": "5.2.16", + "resolved": "https://registry.npmjs.org/ul/-/ul-5.2.16.tgz", + "integrity": "sha512-v1YrSEsJZpJsywzF/MKgsQwMdOwBlwwmNiUOJh/yX6FHrq7dYjeua1YOhLV0q0KioqEFZC4P7MsKmpEsGdZz3w==", + "license": "MIT", + "dependencies": { + "deffy": "^2.2.2", + "typpy": "^2.3.4" + } + }, + "node_modules/undici": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.20.0.tgz", + "integrity": "sha512-MJZrkjyd7DeC+uPZh+5/YaMDxFiiEEaDgbUSVMXayofAkDWF1088CDo+2RPg7B1BuS1qf1vgNE7xqwPxE0DuSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-5.0.0.tgz", + "integrity": "sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/unique-slug": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-6.0.0.tgz", + "integrity": "sha512-4Lup7Ezn8W3d52/xBhZBVdx323ckxa7DEvd9kPQHppTkLoJXw6ltrBCyj5pnrxj0qKDxYMJ56CoxNuFCscdTiw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unzip-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-1.0.2.tgz", + "integrity": "sha512-pwCcjjhEcpW45JZIySExBHYv5Y9EeL2OIGEfrSKp2dMUFGFv4CpvZkwJbVge8OvGH2BNNtJBx67DuKuJhf+N5Q==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "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/uri-js/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/url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha512-BVA4lR5PIviy2PMseNd2jbFQ+jwSwQGdJejf5ctd1rEXt0Ypd7yanUK9+lYechVlN5VaTJGsu2U/3MDDu6KgBA==", + "license": "MIT", + "dependencies": { + "prepend-http": "^1.0.1" + }, + "engines": { + "node": ">=0.10.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==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", + "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/w-json": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/w-json/-/w-json-1.3.10.tgz", + "integrity": "sha512-XadVyw0xE+oZ5FGApXsdswv96rOhStzKqL53uSe5UaTadABGkWIg1+DTx8kiZ/VqTZTBneoL0l65RcPe4W3ecw==", + "license": "MIT" + }, + "node_modules/watchpack": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.0.tgz", + "integrity": "sha512-e6vZvY6xboSwLz2GD36c16+O/2Z6fKvIf4pOXptw2rY9MVwE/TXc6RGqxD3I3x0a28lwBY7DE+76uTPSsBrrCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/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/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "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/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + }, + "node_modules/zone.js": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", + "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", + "license": "MIT" + } + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/package.json b/apps/error-tracking/no-posthog-angular/angular-saas/package.json new file mode 100644 index 000000000..f5499e313 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/package.json @@ -0,0 +1,55 @@ +{ + "name": "angular-boilerplate", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test", + "lint": "ng lint" + }, + "private": true, + "dependencies": { + "@angular/animations": "^21.0.0", + "@angular/common": "^21.0.0", + "@angular/compiler": "^21.0.0", + "@angular/core": "^21.0.0", + "@angular/forms": "^21.0.0", + "@angular/localize": "^21.0.0", + "@angular/platform-browser": "^21.0.0", + "@angular/platform-browser-dynamic": "^21.0.0", + "@angular/router": "^21.0.0", + "@angular/service-worker": "^21.0.0", + "@colsen1991/ngx-translate-extract-marker": "^3.0.1", + "@ngx-translate/core": "^16.0.4", + "@ngx-translate/http-loader": "^16.0.1", + "@ngxpert/hot-toast": "^6.1.0", + "algoliasearch": "^5.46.2", + "bootstrap": "^5.3.0", + "class-transformer": "^0.5.1", + "package.json": "^2.0.1", + "reflect-metadata": "^0.2.0", + "rxjs": "^7.8.2", + "sanitize.css": "^13.0.0", + "socket.io-client": "^4.8.1", + "zone.js": "~0.15.1" + }, + "devDependencies": { + "@angular/build": "^21.0.0", + "@angular/cli": "^21.0.0", + "@angular/compiler-cli": "^21.0.0", + "@angular/language-service": "^21.0.0", + "@types/jasmine": "~5.1.0", + "angular-eslint": "^21.0.0", + "eslint": "^9.30.0", + "jasmine-core": "~5.2.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "typescript": "~5.9.0", + "typescript-eslint": "^8.33.1" + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/public/browserconfig.xml b/apps/error-tracking/no-posthog-angular/angular-saas/public/browserconfig.xml new file mode 100644 index 000000000..704907e6c --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/public/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #da532c + + + diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/public/favicon.ico b/apps/error-tracking/no-posthog-angular/angular-saas/public/favicon.ico new file mode 100644 index 000000000..57614f9c9 Binary files /dev/null and b/apps/error-tracking/no-posthog-angular/angular-saas/public/favicon.ico differ diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/public/icons/icon-128x128.png b/apps/error-tracking/no-posthog-angular/angular-saas/public/icons/icon-128x128.png new file mode 100644 index 000000000..5a9a2ccdb Binary files /dev/null and b/apps/error-tracking/no-posthog-angular/angular-saas/public/icons/icon-128x128.png differ diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/public/icons/icon-144x144.png b/apps/error-tracking/no-posthog-angular/angular-saas/public/icons/icon-144x144.png new file mode 100644 index 000000000..11702cd7b Binary files /dev/null and b/apps/error-tracking/no-posthog-angular/angular-saas/public/icons/icon-144x144.png differ diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/public/icons/icon-152x152.png b/apps/error-tracking/no-posthog-angular/angular-saas/public/icons/icon-152x152.png new file mode 100644 index 000000000..ff4e06b85 Binary files /dev/null and b/apps/error-tracking/no-posthog-angular/angular-saas/public/icons/icon-152x152.png differ diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/public/icons/icon-192x192.png b/apps/error-tracking/no-posthog-angular/angular-saas/public/icons/icon-192x192.png new file mode 100644 index 000000000..afd36a48c Binary files /dev/null and b/apps/error-tracking/no-posthog-angular/angular-saas/public/icons/icon-192x192.png differ diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/public/icons/icon-384x384.png b/apps/error-tracking/no-posthog-angular/angular-saas/public/icons/icon-384x384.png new file mode 100644 index 000000000..613ac793e Binary files /dev/null and b/apps/error-tracking/no-posthog-angular/angular-saas/public/icons/icon-384x384.png differ diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/public/icons/icon-512x512.png b/apps/error-tracking/no-posthog-angular/angular-saas/public/icons/icon-512x512.png new file mode 100644 index 000000000..7574990f2 Binary files /dev/null and b/apps/error-tracking/no-posthog-angular/angular-saas/public/icons/icon-512x512.png differ diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/public/icons/icon-72x72.png b/apps/error-tracking/no-posthog-angular/angular-saas/public/icons/icon-72x72.png new file mode 100644 index 000000000..033724e15 Binary files /dev/null and b/apps/error-tracking/no-posthog-angular/angular-saas/public/icons/icon-72x72.png differ diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/public/icons/icon-96x96.png b/apps/error-tracking/no-posthog-angular/angular-saas/public/icons/icon-96x96.png new file mode 100644 index 000000000..3090dc2d8 Binary files /dev/null and b/apps/error-tracking/no-posthog-angular/angular-saas/public/icons/icon-96x96.png differ diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/public/images/no-image.png b/apps/error-tracking/no-posthog-angular/angular-saas/public/images/no-image.png new file mode 100644 index 000000000..2e0c7c4c9 Binary files /dev/null and b/apps/error-tracking/no-posthog-angular/angular-saas/public/images/no-image.png differ diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/public/images/placeholder.png b/apps/error-tracking/no-posthog-angular/angular-saas/public/images/placeholder.png new file mode 100644 index 000000000..fe7683c3c Binary files /dev/null and b/apps/error-tracking/no-posthog-angular/angular-saas/public/images/placeholder.png differ diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/public/manifest.webmanifest b/apps/error-tracking/no-posthog-angular/angular-saas/public/manifest.webmanifest new file mode 100644 index 000000000..7c1ecb2ee --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/public/manifest.webmanifest @@ -0,0 +1,59 @@ +{ + "name": "angular-boilerplate", + "short_name": "angular-boilerplate", + "theme_color": "#1976d2", + "background_color": "#fafafa", + "display": "standalone", + "scope": "./", + "start_url": "./", + "icons": [ + { + "src": "icons/icon-72x72.png", + "sizes": "72x72", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "icons/icon-96x96.png", + "sizes": "96x96", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "icons/icon-128x128.png", + "sizes": "128x128", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "icons/icon-144x144.png", + "sizes": "144x144", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "icons/icon-152x152.png", + "sizes": "152x152", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "icons/icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "icons/icon-384x384.png", + "sizes": "384x384", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "icons/icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable any" + } + ] +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/public/robots.txt b/apps/error-tracking/no-posthog-angular/angular-saas/public/robots.txt new file mode 100644 index 000000000..d0e5f1be6 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/public/robots.txt @@ -0,0 +1,5 @@ +# www.robotstxt.org/ + +# Allow crawling of all content +User-agent: * +Disallow: diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/constants/app-settings.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/constants/app-settings.ts new file mode 100644 index 000000000..0852c471e --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/constants/app-settings.ts @@ -0,0 +1,55 @@ +import { ROLE } from '../../auth/enums/roles.enum'; + +// TODO: Implement the permissions as per your application requirements. +// Either fetch it from the server or define it in the client. if you have constant permissions, you can define it in below format and move this in core constants folder. +// If you have different structure, you have to modify the permissions setup as per your requirement in this file. + +/** + * The `appSetting` object contains the role-based permissions for the application. + * The permission setup is based on the role-based access control (RBAC) model. In this model, permissions are assigned to roles, and roles are assigned to users. + * For example, you can define the permissions in the following format: + */ +export const appSetting = { + role: { + [ROLE.ADMIN]: { + create: { + user: true, + sale: true, + }, + access: { + user: true, + sale: true, + }, + }, + [ROLE.USER]: { + create: { + user: false, + sale: false, + }, + access: { + user: false, + sale: true, + }, + }, + [ROLE.MEMBER]: { + create: { + user: false, + sale: true, + }, + access: { + user: false, + sale: true, + }, + }, + [ROLE.GUEST]: { + create: { + user: false, + sale: false, + }, + access: { + user: false, + sale: false, + }, + }, + }, +}; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/constants/index.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/constants/index.ts new file mode 100644 index 000000000..559270920 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/constants/index.ts @@ -0,0 +1,2 @@ +export * from './app-settings'; +export * from './nav-menu-items'; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/constants/nav-menu-items.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/constants/nav-menu-items.ts new file mode 100644 index 000000000..a42b10dd2 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/constants/nav-menu-items.ts @@ -0,0 +1,36 @@ +import { PERMISSIONS } from '../../auth/enums/permissions.enum'; +import { NavMenuItem } from '@core/interfaces'; + +// THIS FILE CONTAINS THE NAVIGATION MENU ITEMS FOR THE SIDEBAR AND ALL OTHER NAVIGATION MENUS WHICH ARE USED IN THE APPLICATION AND ARE CONSTANT + +/** + * Navigation menu items for WEB Sidebar + */ +export const webSidebarMenuItems: NavMenuItem[] = [ + { + href: '/dashboard', + title: 'Dashboard', + active: true, + icon: '🏠', + }, + { + href: '/users', + title: 'Team', + active: false, + icon: '👥', + permissions: [PERMISSIONS.ACCESS_USER], + }, + { + href: '/billing', + title: 'Billing', + active: false, + icon: '💳', + divider: true, + }, + { + href: '/settings', + title: 'Settings', + active: false, + icon: '⚙️', + }, +]; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/entities/_extra/base.entity.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/entities/_extra/base.entity.ts new file mode 100644 index 000000000..a372350f8 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/entities/_extra/base.entity.ts @@ -0,0 +1,22 @@ +import { Expose } from 'class-transformer'; +import { InitializableEntity } from '@core/entities/_extra/intializable.entity'; + +export class BaseEntity extends InitializableEntity { + id: number; + + @Expose({ name: 'created_at' }) + createdAt: string; + + @Expose({ name: 'updated_at' }) + updatedAt: string; + + modifiedAt: string; + + @Expose({ name: 'created_by' }) + createdBy = 0; + + constructor(init?: Partial) { + super(init); + this.initEntity(init); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/entities/_extra/index.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/entities/_extra/index.ts new file mode 100644 index 000000000..0e8379c07 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/entities/_extra/index.ts @@ -0,0 +1,2 @@ +export * from './base.entity'; +export * from './paginated.entity'; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/entities/_extra/intializable.entity.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/entities/_extra/intializable.entity.ts new file mode 100644 index 000000000..26395f533 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/entities/_extra/intializable.entity.ts @@ -0,0 +1,46 @@ +/** + * This class is used to initialize an entity with the given values. + * It is used to avoid the need to write a constructor for each entity. + * + * @export + * @class InitializableEntity + * @template T + * @param {Partial} [initialValues] + * @param {Partial} [initialValues] + * @param {Partial} [initialValues] + * + * @example + * export class User extends InitializableEntity { + * id: number + * name: string + * email: string + * constructor (initialValues?: Partial) { + * super(initialValues) + * } + * } + * const user = new User({ id: 1, name: 'John Doe' }) + * + * console.log(user.id) // 1 + * console.log(user.name) // 'John Doe' + * console.log(user.email) // undefined + + */ + +export class InitializableEntity { + constructor(initialValues?: Record) { + this.initEntity(initialValues); + } + + protected initEntity(initialValues?: Partial): void { + if (initialValues) { + // Convert a possible class to object + for (const k in this) { + // @ts-ignore + if (initialValues[k]) { + // @ts-ignore + this[k] = initialValues[k]; + } + } + } + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/entities/_extra/paginated.entity.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/entities/_extra/paginated.entity.ts new file mode 100644 index 000000000..824628d39 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/entities/_extra/paginated.entity.ts @@ -0,0 +1,21 @@ +import { InitializableEntity } from '@core/entities/_extra/intializable.entity'; + +export class PaginatedEntity extends InitializableEntity { + pagination: Pagination; + + constructor(values?: Partial) { + super(); + this.pagination = { + page: 1, + pageSize: 10, + }; + this.initEntity(values); + } +} + +export interface Pagination { + page: number; + pageSize: number; + rowCount?: number; + pageCount?: number; +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/entities/credentials.entity.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/entities/credentials.entity.ts new file mode 100644 index 000000000..b9dd9589a --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/entities/credentials.entity.ts @@ -0,0 +1,30 @@ +import { Expose } from 'class-transformer'; +import { InitializableEntity } from '@core/entities/_extra/intializable.entity'; + +export class Credentials extends InitializableEntity { + id = ''; + username = ''; + firstName? = ''; + lastName? = ''; + email? = ''; + + @Expose({ name: 'access_token' }) + token = ''; + + @Expose({ name: 'expires_in' }) + expiresIn = 0; + + @Expose({ name: 'refresh_token' }) + refreshToken = ''; + + roles: string[] = []; + + constructor(init?: Partial) { + super(init); + this.initEntity(init); + } + + get fullName() { + return this.firstName + ' ' + this.lastName; + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/entities/index.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/entities/index.ts new file mode 100644 index 000000000..14b6d02c1 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/entities/index.ts @@ -0,0 +1,8 @@ +// extra folder +export * from './_extra'; + +export * from './credentials.entity'; + +// Demo purposes only + +export * from './random-user.entity'; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/entities/random-user.entity.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/entities/random-user.entity.ts new file mode 100644 index 000000000..c58e9020c --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/entities/random-user.entity.ts @@ -0,0 +1,91 @@ +import { Type } from 'class-transformer'; + +export class RandomUserEntity { + gender: string; + + @Type(() => Name) + name: Name; + + @Type(() => Location) + location: Location; + + email: string; + login: Login; + dob: Dob; + registered: Registered; + phone: string; + cell: string; + id: Id; + picture: Picture; + nat: string; +} + +class Name { + title: string; + first: string; + last: string; + + get fullName(): string { + return `${this.first} ${this.last}`; + } +} + +class Location { + street: Street; + city: string; + state: string; + country: string; + postcode: number; + coordinates: Coordinates; + timezone: Timezone; + + get fullAddress(): string { + return `${this.street.number} ${this.street.name}, ${this.city}, ${this.state}, ${this.country}, ${this.postcode}`; + } +} + +class Street { + number: number; + name: string; +} + +class Coordinates { + latitude: string; + longitude: string; +} + +class Timezone { + offset: string; + description: string; +} + +class Login { + uuid: string; + username: string; + password: string; + salt: string; + md5: string; + sha1: string; + sha256: string; +} + +class Dob { + date: string; + age: number; +} + +class Registered { + date: string; + age: number; +} + +class Id { + name: string; + value: any; +} + +class Picture { + large: string; + medium: string; + thumbnail: string; +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/enums/index.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/enums/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/guards/index.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/guards/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/helpers/cacheable.decorator.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/helpers/cacheable.decorator.ts new file mode 100644 index 000000000..c1853a518 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/helpers/cacheable.decorator.ts @@ -0,0 +1,82 @@ +import { Observable, of, throwError } from 'rxjs'; +import { catchError, shareReplay, tap } from 'rxjs/operators'; +import { CacheService } from '@core/services'; + +/** + * The `Cacheable` function is a TypeScript decorator that adds caching functionality to a method. + * @requires CacheService - The `Cacheable` decorator requires the `CacheService` to be available on the target (Parent) class. + * @param {string} cacheKey - The `cacheKey` parameter is a string that represents the key used to + * identify the cache entry. It is combined with the serialized arguments of the method to create a + * unique cache key for each method call. + * @param [options] - The `options` parameter is an optional object that can contain the following + * properties: + * - `ttl` - The `ttl` property is a number that represents the time to live (in milliseconds) of + * the cache entry. If the cache entry is older than the time to live, it is considered invalid and + * will be evicted from the cache. + * - `maxSize` - The `maxSize` property is a number that represents the maximum number of entries + * that can be stored in the cache. If the cache size exceeds the maximum size, the least recently + * accessed entry will be evicted from the cache. + * - `handleError` - The `handleError` property is a boolean that indicates whether to handle errors + * or not. If set to `true`, the decorator will catch errors and return an empty observable instead. + * If set to `false`, the decorator will rethrow the error. + * @returns The `Cacheable` function returns a decorator function. + * @example + * // The following code snippet shows how to use the `Cacheable` decorator. + * import { Cacheable } from '@core/helpers'; + * + * @Cacheable('getIcon', { maxSize: 150 }) + * get(name: string): Observable { + * return this.http.get(, { responseType: 'text' }); + * } + */ +export function Cacheable(cacheKey: string, options?: { ttl?: number; maxSize?: number; handleError?: boolean }) { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + const originalMethod = descriptor.value; + + descriptor.value = function (...args: any[]) { + if (!this._cacheService) { + console.warn('CacheService is not available on the target class'); + return originalMethod.apply(this, args); + } + + const cacheService: CacheService = this._cacheService; + + const normalizedArgs = JSON.stringify(args); + const key = `${cacheKey}-${normalizedArgs}`; + const now = Date.now(); + + const cacheEntry = cacheService.get(key); + if (cacheEntry && (!options?.ttl || now - cacheEntry.timestamp < options.ttl)) { + cacheEntry.lastAccessed = now; + cacheService.set(key, cacheEntry.data); // Update lastAccessed time + return of(cacheEntry.data); + } + + const inFlight = cacheService.getInFlightRequest(key); + if (inFlight) { + return inFlight; + } + + const resultObservable: Observable = originalMethod.apply(this, args).pipe( + tap((result) => { + cacheService.set(key, result); + cacheService.deleteInFlightRequest(key); + if (options?.maxSize && cacheService.getSize() >= options.maxSize) { + cacheService.evictCache(Math.ceil(options.maxSize * 0.1)); + } + }), + catchError((err) => { + cacheService.deleteInFlightRequest(key); + return throwError(err); + }), + shareReplay(1), + ); + + cacheService.setInFlightRequest(key, resultObservable); + + return resultObservable; + }; + + return descriptor; + }; +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/helpers/index.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/helpers/index.ts new file mode 100644 index 000000000..14b7e3fd6 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/helpers/index.ts @@ -0,0 +1,2 @@ +export * from './route-reusable-strategy'; +export * from './cacheable.decorator'; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/helpers/route-reusable-strategy.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/helpers/route-reusable-strategy.ts new file mode 100644 index 000000000..728e81059 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/helpers/route-reusable-strategy.ts @@ -0,0 +1,30 @@ +import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from '@angular/router'; +import { Injectable } from '@angular/core'; + +/** + * A route strategy allowing for explicit route reuse. + * Used as a workaround for https://github.com/angular/angular/issues/18374 + * To reuse a given route, add `data: { reuse: true }` to the route definition. + */ +@Injectable() +export class RouteReusableStrategy extends RouteReuseStrategy { + public shouldDetach(route: ActivatedRouteSnapshot): boolean { + return false; + } + + public store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle | null): void {} + + public shouldAttach(route: ActivatedRouteSnapshot): boolean { + return false; + } + + public retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null { + return null; + } + + public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { + // Reuse the route if the RouteConfig is the same, or if both routes use the + // same component, because the latter can have different RouteConfigs. + return future.routeConfig === curr.routeConfig || Boolean(future.routeConfig?.component && future.routeConfig?.component === curr.routeConfig?.component); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/interceptors/api-prefix.interceptor.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/interceptors/api-prefix.interceptor.ts new file mode 100644 index 000000000..134d86e41 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/interceptors/api-prefix.interceptor.ts @@ -0,0 +1,73 @@ +import { Injectable } from '@angular/core'; +import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { Observable, Subject, takeUntil, throwError } from 'rxjs'; +import { TranslateService } from '@ngx-translate/core'; +import { catchError, finalize } from 'rxjs/operators'; +import { CredentialsService } from '@auth'; + +@Injectable({ + providedIn: 'root', +}) +export class ApiPrefixInterceptor implements HttpInterceptor { + private readonly _ongoingRequests = new Map>(); + + constructor( + private readonly _credentialsService: CredentialsService, + private readonly _translateService: TranslateService, + ) {} + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + // If the request has the 'noauth' header, don't add the Authorization header + if (request.headers.get('noauth')) { + return next.handle(request); + } + + let headers = request.headers; + const { token } = this._credentialsService.credentials() || {}; + const currentLang = this._translateService.currentLang.substring(0, 2); + + if (token) { + if (!(request.body instanceof FormData)) { + headers = headers.set('content-type', 'application/json'); + } + headers = headers.set('Authorization', `Bearer ${token}`); + } + headers = headers.set('Accept-Language', currentLang).set('Content-Language', currentLang).set('lang', currentLang); + + request = request.clone({ headers }); + + /** + * In below piece of code If there is an ongoing request with the same method and URL than return the ongoing request instead of creating a new one + * This is useful when the user clicks multiple times on a button that triggers a request + */ + + const requestKey = this._getRequestKey(request); + + const ongoingRequest = this._ongoingRequests.get(requestKey); + if (ongoingRequest) { + return ongoingRequest.asObservable(); + } else { + const cancelSubject = new Subject(); + this._ongoingRequests.set(requestKey, cancelSubject); + + return next.handle(request).pipe( + takeUntil(cancelSubject), + catchError((error: HttpErrorResponse) => { + if (error.status === 401 || error.status === 403) { + this._credentialsService.setCredentials(); + window.location.href = '/login'; + } + return throwError(error); + }), + finalize(() => { + this._ongoingRequests.delete(requestKey); + cancelSubject.complete(); + }), + ); + } + } + + private _getRequestKey(req: HttpRequest): string { + return `${req.method} ${req.urlWithParams}`; + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/interceptors/error-handler.interceptor.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/interceptors/error-handler.interceptor.ts new file mode 100644 index 000000000..4d22fe351 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/interceptors/error-handler.interceptor.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@angular/core'; +import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { catchError } from 'rxjs/operators'; + +import { environment } from '@env/environment'; +import { Logger } from '../services/misc'; + +const log = new Logger('ErrorHandlerInterceptor'); + +/** + * Adds a default error handler to all requests. + */ +@Injectable({ + providedIn: 'root', +}) +export class ErrorHandlerInterceptor implements HttpInterceptor { + intercept(request: HttpRequest, next: HttpHandler): Observable> { + return next.handle(request).pipe(catchError((error) => this._errorHandler(error))); + } + + //TODO: Customize the default error handler here if needed + private _errorHandler(response: HttpEvent): Observable> { + if (!environment.production) { + // Do something with the error + log.error('Request error', response); + } + throw response; + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/interceptors/index.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/interceptors/index.ts new file mode 100644 index 000000000..049985fe6 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/interceptors/index.ts @@ -0,0 +1,2 @@ +export * from './api-prefix.interceptor'; +export * from './error-handler.interceptor'; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/interfaces/index.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/interfaces/index.ts new file mode 100644 index 000000000..98ac58e44 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/interfaces/index.ts @@ -0,0 +1 @@ +export * from './navMenuItem.interface'; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/interfaces/navMenuItem.interface.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/interfaces/navMenuItem.interface.ts new file mode 100644 index 000000000..772023c6d --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/interfaces/navMenuItem.interface.ts @@ -0,0 +1,12 @@ +export interface NavMenuItem { + title: string; + href: string; + icon?: string; + url?: string; + active?: boolean; + subItems?: NavMenuItem[]; + disabled?: boolean; + divider?: boolean; + roles?: string[]; + permissions?: string[]; +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/services/data.service.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/services/data.service.ts new file mode 100644 index 000000000..dc0f45691 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/services/data.service.ts @@ -0,0 +1,189 @@ +import { computed, Injectable, signal } from '@angular/core'; +import { toObservable } from '@angular/core/rxjs-interop'; +import { Observable } from 'rxjs'; + +export interface Project { + id: string; + name: string; + description: string; + status: 'active' | 'completed' | 'on-hold'; + createdAt: Date; + memberIds: string[]; +} + +export interface Member { + id: string; + name: string; + email: string; + role: 'admin' | 'member' | 'viewer'; + avatar: string; + joinedAt: Date; +} + +export interface Activity { + id: string; + user: string; + action: string; + target: string; + time: Date; + avatar: string; +} + +@Injectable({ + providedIn: 'root', +}) +export class DataService { + /** Projects signal */ + readonly projects = signal([ + { + id: '1', + name: 'Marketing Campaign', + description: 'Q1 2024 marketing initiative', + status: 'active', + createdAt: new Date('2024-01-15'), + memberIds: ['1', '2'], + }, + { + id: '2', + name: 'Product Launch', + description: 'New feature rollout', + status: 'active', + createdAt: new Date('2024-01-20'), + memberIds: ['1'], + }, + ]); + + /** Members signal */ + readonly members = signal([ + { + id: '1', + name: 'Sarah Chen', + email: 'sarah@example.com', + role: 'admin', + avatar: '👩', + joinedAt: new Date('2023-06-01'), + }, + { + id: '2', + name: 'Alex Rivera', + email: 'alex@example.com', + role: 'member', + avatar: '👨', + joinedAt: new Date('2023-08-15'), + }, + ]); + + /** Activities signal */ + readonly activities = signal([]); + + /** Computed stats signal */ + readonly stats = computed(() => ({ + totalProjects: this.projects().length, + activeProjects: this.projects().filter((p) => p.status === 'active').length, + totalMembers: this.members().length, + recentActivities: this.activities().length, + })); + + // Observable bridges for legacy consumers + readonly projects$ = toObservable(this.projects); + readonly members$ = toObservable(this.members); + readonly activities$ = toObservable(this.activities); + + constructor() { + // Initialize with some activities + this.addActivity('System', 'initialized', 'workspace'); + } + + // Projects + getProjects(): Observable { + return this.projects$; + } + + getProjectsSnapshot(): Project[] { + return this.projects(); + } + + addProject(project: Omit): Project { + const newProject: Project = { + ...project, + id: crypto.randomUUID(), + createdAt: new Date(), + memberIds: [], + }; + this.projects.update((current) => [newProject, ...current]); + this.addActivity('You', 'created project', project.name); + return newProject; + } + + updateProject(id: string, updates: Partial): void { + this.projects.update((current) => current.map((p) => (p.id === id ? { ...p, ...updates } : p))); + } + + deleteProject(id: string): void { + const project = this.projects().find((p) => p.id === id); + this.projects.update((current) => current.filter((p) => p.id !== id)); + if (project) { + this.addActivity('You', 'deleted project', project.name); + } + } + + // Members + getMembers(): Observable { + return this.members$; + } + + getMembersSnapshot(): Member[] { + return this.members(); + } + + addMember(member: Omit): Member { + const newMember: Member = { + ...member, + id: crypto.randomUUID(), + joinedAt: new Date(), + }; + this.members.update((current) => [newMember, ...current]); + this.addActivity('You', 'added team member', member.name); + return newMember; + } + + updateMember(id: string, updates: Partial): void { + this.members.update((current) => current.map((m) => (m.id === id ? { ...m, ...updates } : m))); + } + + deleteMember(id: string): void { + const member = this.members().find((m) => m.id === id); + this.members.update((current) => current.filter((m) => m.id !== id)); + if (member) { + this.addActivity('You', 'removed team member', member.name); + } + } + + // Activities + getActivities(): Observable { + return this.activities$; + } + + getActivitiesSnapshot(): Activity[] { + return this.activities(); + } + + addActivity(user: string, action: string, target: string): void { + const avatars = ['👤', '👩', '👨', '🧑', '👩‍💼', '👨‍💼']; + const activity: Activity = { + id: crypto.randomUUID(), + user, + action, + target, + time: new Date(), + avatar: user === 'You' ? '👤' : avatars[Math.floor(Math.random() * avatars.length)], + }; + // Keep only last 20 activities + this.activities.update((current) => [activity, ...current].slice(0, 20)); + } + + // Stats (legacy method - prefer using stats signal directly) + getStats() { + return this.stats(); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/services/index.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/services/index.ts new file mode 100644 index 000000000..348fac120 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/services/index.ts @@ -0,0 +1,4 @@ +// Misc Services +export * from './misc'; + +export * from './random-user.service'; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/services/misc/cache.service.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/services/misc/cache.service.ts new file mode 100644 index 000000000..dfc2c6ed1 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/services/misc/cache.service.ts @@ -0,0 +1,109 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +interface CacheContent { + data: any; + timestamp: number; + lastAccessed: number; +} + +@Injectable({ + providedIn: 'root', +}) +export class CacheService { + private readonly _cache = new Map(); + private readonly _inFlightRequests = new Map>(); + + /** + * The `get` function retrieves the value associated with a specified key from the cache. + * @param {string} key - The `get` method in the code snippet you provided takes a `key` parameter of + * type `string`. This key is used to retrieve a value from the cache. + * @returns The `get` method is returning the value associated with the provided `key` in the cache. If + * there is no value associated with the key, it will return `undefined`. + */ + get(key: string): CacheContent | undefined { + return this._cache.get(key); + } + + /** + * This TypeScript function returns the size of a cache. + * @returns The `getSize()` method is returning the size of the `_cache` property, which is a number. + */ + getSize(): number { + return this._cache.size; + } + + /** + * The `set` function in TypeScript sets a key-value pair in a cache with a timestamp and last accessed + * time. + * @param {string} key - The `key` parameter is a string that serves as the identifier for the data + * being stored in the cache. + * @param {any} data - The `data` parameter in the `set` method is used to store any type of data that + * you want to associate with the specified key in the cache. This data can be of any JavaScript data + * type such as string, number, object, array, etc. + */ + set(key: string, data: any): void { + const now = Date.now(); + this._cache.set(key, { data, timestamp: now, lastAccessed: now }); + } + + /** + * This TypeScript function deletes a key from a cache. + * @param {string} key - The `key` parameter is a string that represents the key of the item you want + * to delete from the cache. + */ + delete(key: string): void { + this._cache.delete(key); + } + + /** + * The `has` function in TypeScript checks if a key exists in a cache. + * @param {string} key - The `key` parameter in the `has` method is a string that represents the key + * you want to check for existence in the cache. + * @returns The `has` method is returning a boolean value, which indicates whether the key exists in + * the cache or not. + */ + has(key: string): boolean { + return this._cache.has(key); + } + + /** + * The function `getInFlightRequest` retrieves an in-flight request based on a given key. + * @param {string} key - The `key` parameter is a string that is used to retrieve a value from a + * collection of in-flight requests. + * @returns The `getInFlightRequest` method is returning an `Observable` or `undefined` value + * based on the key provided. + */ + getInFlightRequest(key: string): Observable | undefined { + return this._inFlightRequests.get(key); + } + + /** + * The function `setInFlightRequest` stores an observable with a specified key in a map. + * @param {string} key - The `key` parameter is a string that serves as a unique identifier for the + * in-flight request. + * @param observable - An Observable is a representation of any set of values over any amount of time. + * It is used in reactive programming to handle asynchronous data streams. + */ + setInFlightRequest(key: string, observable: Observable): void { + this._inFlightRequests.set(key, observable); + } + + /** + * The function `deleteInFlightRequest` removes a key from a Map object `_inFlightRequests`. + * @param {string} key - The `key` parameter in the `deleteInFlightRequest` function is a string type + * that represents the key of the request to be deleted from the `_inFlightRequests` map. + */ + deleteInFlightRequest(key: string): void { + this._inFlightRequests.delete(key); + } + + /** + * The function `evictCache` evicts the least recently accessed entry from a cache. + * @param count - The `count` parameter is a number that represents the number of cache + */ + evictCache(count = 1): void { + const entries = Array.from(this._cache.entries()).sort((a, b) => a[1].lastAccessed - b[1].lastAccessed); + entries.slice(0, count).forEach(([key]) => this._cache.delete(key)); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/services/misc/index.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/services/misc/index.ts new file mode 100644 index 000000000..40bf9644e --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/services/misc/index.ts @@ -0,0 +1,3 @@ +export * from './cache.service'; +export * from './logger.service'; +export * from './update.service'; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/services/misc/logger.service.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/services/misc/logger.service.ts new file mode 100644 index 000000000..bbc11a4d4 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/services/misc/logger.service.ts @@ -0,0 +1,111 @@ +/** + * Simple logger system with the possibility of registering custom outputs. + * + * 4 different log levels are provided, with corresponding methods: + * - debug : for debug information + * - info : for informative status of the application (success, ...) + * - warning : for non-critical errors that do not prevent normal application behavior + * - error : for critical errors that prevent normal application behavior + * + * Example usage: + * ``` + * import { Logger } from 'app/core/logger.service'; + * + * const log = new Logger('myFile'); + * ... + * log.debug('something happened'); + * ``` + * + * To disable debug and info logs in production, add this snippet to your root component: + * ``` + * export class AppComponent implements OnInit { + * ngOnInit() { + * if (environment.production) { + * Logger.enableProductionMode(); + * } + * ... + * } + * } + * + * If you want to process logs through other outputs than console, you can add LogOutput functions to Logger.outputs. + */ + +/** + * The possible log levels. + * LogLevel.Off is never emitted and only used with Logger.level property to disable logs. + */ +export enum LogLevel { + Off = 0, + Error, + Warning, + Info, + Debug, +} + +/** + * Log output handler function. + */ +export type LogOutput = (source: string | undefined, level: LogLevel, ...objects: any[]) => void; + +export class Logger { + /** + * Current logging level. + * Set it to LogLevel.Off to disable logs completely. + */ + static level = LogLevel.Debug; + + /** + * Additional log outputs. + */ + static outputs: LogOutput[] = []; + + /** + * Enables production mode. + * Sets logging level to LogLevel.Warning. + */ + static enableProductionMode() { + Logger.level = LogLevel.Warning; + } + + constructor(private readonly _source?: string) {} + + /** + * Logs messages or objects with the debug level. + * Works the same as console.log(). + */ + debug(...objects: any[]) { + this._log(console.log, LogLevel.Debug, objects); + } + + /** + * Logs messages or objects with the info level. + * Works the same as console.log(). + */ + info(...objects: any[]) { + this._log(console.info, LogLevel.Info, objects); + } + + /** + * Logs messages or objects with the warning level. + * Works the same as console.log(). + */ + warn(...objects: any[]) { + this._log(console.warn, LogLevel.Warning, objects); + } + + /** + * Logs messages or objects with the error level. + * Works the same as console.log(). + */ + error(...objects: any[]) { + this._log(console.error, LogLevel.Error, objects); + } + + private _log(func: (...args: any[]) => void, level: LogLevel, objects: any[]) { + if (level <= Logger.level) { + const log = this._source ? ['[' + this._source + ']'].concat(objects) : objects; + func.apply(console, log); + Logger.outputs.forEach((output) => output.apply(output, [this._source, level, ...objects])); + } + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/services/misc/update.service.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/services/misc/update.service.ts new file mode 100644 index 000000000..026e49fed --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/services/misc/update.service.ts @@ -0,0 +1,100 @@ +import { ApplicationRef, Component, DestroyRef, Inject, inject, Injectable, Optional } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { SwUpdate } from '@angular/service-worker'; +import { concat, from, interval, of } from 'rxjs'; +import { catchError, first, startWith, switchMap } from 'rxjs/operators'; +import { HotToastRef, HotToastService } from '@ngxpert/hot-toast'; + +/* The `AppUpdateService` is responsible for checking for app updates using a +service worker and displaying update alerts to the user. */ +@Injectable({ + providedIn: 'root', +}) +export class AppUpdateService { + private readonly swUpdate = inject(SwUpdate); + private readonly toastService = inject(HotToastService); + private readonly appRef = inject(ApplicationRef); + private readonly destroyRef = inject(DestroyRef); + + private _isUpdateToastShown = false; + + constructor() { + console.log('%c Update service is running...', 'color: green; font-weight: bold;'); + + if (this.swUpdate?.isEnabled) { + console.log('%c Service worker enabled', 'color: orange; font-weight: bold;'); + + // Allow the app to stabilize first, before starting polling for updates. + const appIsStable$ = this.appRef?.isStable?.pipe(first((isStable) => isStable === true)); + const everySixHours$ = interval(1000 * 60 * 60 * 6).pipe(startWith(0)); + const everySixHoursOnceAppIsStable$ = concat(appIsStable$, everySixHours$); + + everySixHoursOnceAppIsStable$ + .pipe( + switchMap(() => { + console.log('%c Checking for app updates...', 'color: yellow; font-weight: bold;'); + this._isUpdateToastShown = false; + return from(this.swUpdate.checkForUpdate()).pipe( + catchError((err) => { + console.error('Failed to check for updates:', err); + return of(false); + }), + ); + }), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe((updateFound) => { + console.log('%c Finish checking for updates...', 'color: yellow; font-weight: bold;'); + console.log(updateFound ? '%c A new version is available.' : '%c Already on the latest version.', 'color: white; font-weight: bold;'); + }); + } else { + console.log('%c No service worker allow', 'color: red; font-weight: bold;'); + } + } + + subscribeForUpdates(): void { + this.swUpdate?.versionUpdates?.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((evt) => { + switch (evt.type) { + case 'VERSION_DETECTED': + console.log(`%c Downloading new app version: ${evt.version.hash}`, 'color: green;'); + break; + case 'VERSION_READY': + console.log(`Current app version: ${evt.currentVersion.hash}`); + console.log(`%c New app version ready for use: ${evt.latestVersion.hash}`, 'color: cyan; font-weight: bold;'); + this._showAppUpdateAlert(); + break; + case 'VERSION_INSTALLATION_FAILED': + console.log(`%c Failed to install app version '${evt.version.hash}': ${evt.error}`, 'color: red; font-weight: bold;'); + break; + } + }); + } + + private _showAppUpdateAlert() { + if (this._isUpdateToastShown) { + return; + } + this._isUpdateToastShown = true; + const toastRef = this.toastService.show(UpdateComponent, { + autoClose: false, + dismissible: false, + }); + toastRef.afterClosed.subscribe(() => { + this.swUpdate.activateUpdate().then(() => document.location.reload()); + this._isUpdateToastShown = false; + }); + } +} + +// App Update Notification Component +@Component({ + selector: 'app-update-component', + template: ` + New Version is Available. + Please Click to Update + or Close + `, +}) +export class UpdateComponent { + constructor(@Optional() @Inject(HotToastRef) public toastRef: HotToastRef) {} +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/services/random-user.service.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/services/random-user.service.ts new file mode 100644 index 000000000..05458f179 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/services/random-user.service.ts @@ -0,0 +1,20 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { map } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class RandomUserService { + private readonly http = inject(HttpClient); + + find() { + return this.http + .get('https://randomuser.me/api/', { + headers: { + noauth: 'true', // Ad noauth header to bypass custom api prefix interceptor + }, + }) + .pipe(map((response: any) => response.results)); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/socket-io/config/socket-io.config.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/socket-io/config/socket-io.config.ts new file mode 100644 index 000000000..912952abb --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/socket-io/config/socket-io.config.ts @@ -0,0 +1,126 @@ +/** Config interface */ +export interface SocketIoConfig { + rootUrl: string; + /** + * Options + * References: + * https://github.com/socketio/socket.io-client/blob/master/docs/API.md#new-managerurl-options + */ + options?: { + /** + * Name of the path that is captured on the server side. Default: /socket.io + */ + path?: string; + /** + * Whether to reconnect automatically. Default: true + */ + reconnection?: boolean; + /** + * Number of reconnection attempts before giving up. Default: infinity + */ + reconnectionAttempts?: number; + + /** + * How long to initially wait before attempting a new reconnection. Default: 1000 +- randomizationFactor + */ + reconnectionDelay?: number; + + /** + * Maximum amount of time to wait between reconnections. Default: 5000 + */ + reconnectionDelayMax?: number; + /** + * Randomization factor for the reconnection delay. Default: 0.5 + */ + randomizationFactor?: number; + /** + * Connection timeout before a connect_error and connect_timeout events are emitted. Default: 20000 + */ + timeout?: number; + /** + * By setting this false, you have to call manager.open whenever you decide it's appropriate. Default: true + */ + autoConnect?: boolean; + /** + * Additional query parameters that are sent when connecting a namespace (then found in socket.handshake.query object on the server-side) + */ + query?: { + [key: string]: string | null; + }; + /** + * The parser to use. Defaults to an instance of the Parser that ships with Socket.IO + * Reference: https://github.com/socketio/socket.io-parser + */ + parser?: any; + + // Options for the underlying Engine.IO client: + /** + * Whether the client should try to upgrade the transport from long-polling to something better. Default: true + */ + upgrade?: boolean; + /** + * Forces JSONP for polling transport. Default: false + */ + forceJSONP?: boolean; + /** + * Determines whether to use JSONP when necessary for polling. If disabled (by settings to false) an error will be emitted (saying “No transports available”) if no other transports are available. If another transport is available for opening a connection (e.g. WebSocket) that transport will be used instead. Default: false + */ + jsonp?: boolean; + /** + * Forces base 64 encoding for polling transport even when XHR2 responseType is available and WebSocket even if the used standard supports binary. Default: false + */ + forceBase64?: boolean; + /** + * Enables XDomainRequest for IE8 to avoid loading bar flashing with click sound. default to false because XDomainRequest has a flaw of not sending cookie. Default: false + */ + enablesXDR?: boolean; + /** + * Whether to add the timestamp with each transport request. Note: polling requests are always stamped unless this option is explicitly set to false. + */ + timestampRequests?: boolean; + /** + * The timestamp parameter + */ + timestampParam?: string; + /** + * Port the policy server listens on. Default: 843 + */ + policyPort?: number; + /** + * A list of transports to try (in order). Engine always attempts to connect directly with the first one, provided the feature detection test for it passes. Default: ["polling", "websocket"] + */ + transports?: string[]; + /** + * Hash of options, indexed by transport name, overriding the common options for the given transport. Default: {} + */ + transportOptions?: any; + /** + * If true and if the previous websocket connection to the server succeeded, the connection attempt will bypass the normal upgrade process and will initially try websocket. A connection attempt following a transport error will use the normal upgrade process. It is recommended you turn this on only when using SSL/TLS connections, or if you know that your network does not block websockets. Default: false. + */ + rememberUpgrade?: boolean; + /** + * Whether transport upgrades should be restricted to transports supporting binary data. Default: false + */ + onlyBinaryUpgrades?: boolean; + /** + * Timeout for xhr-polling requests in milliseconds (0) (only for polling transport). Default: 0 + */ + requestTimeout?: number; + /** + * A list of subprotocols. See https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#Subprotocols + */ + protocols?: any; + /** + * Whether or not cross-site requests should made using credentials such as cookies, authorization headers or TLS client certificates. Setting withCredentials has no effect on same-site requests. Default value: false + */ + withCredentials?: boolean; + /** + * Additional headers (then found in socket.handshake.headers object on the server-side). Default value: - + */ + extraHeaders?: { + [header: string]: string; + }; + + // Additional options for NodeJS Engine.IO clients omitted: https://socket.io/docs/client-api/ + }; +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/socket-io/index.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/socket-io/index.ts new file mode 100644 index 000000000..68250f110 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/socket-io/index.ts @@ -0,0 +1,3 @@ +export * from './socket-io.module'; +export { WrappedSocket as SocketIoService } from './socket-io.service'; +export * from './config/socket-io.config'; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/socket-io/socket-io.module.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/socket-io/socket-io.module.ts new file mode 100644 index 000000000..297fa94ef --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/socket-io/socket-io.module.ts @@ -0,0 +1,26 @@ +import { InjectionToken, EnvironmentProviders, makeEnvironmentProviders } from '@angular/core'; +import { SocketIoConfig } from './config/socket-io.config'; +import { WrappedSocket } from './socket-io.service'; + +/** Socket factory */ +export function SocketFactory(config: SocketIoConfig) { + return new WrappedSocket(config); +} + +export const SOCKET_CONFIG_TOKEN = new InjectionToken('__SOCKET_IO_CONFIG__'); + +/** + * Provides SocketIO configuration for the application. + * @param config The SocketIO configuration + * @returns Environment providers for SocketIO + */ +export function provideSocketIo(config: SocketIoConfig): EnvironmentProviders { + return makeEnvironmentProviders([ + { provide: SOCKET_CONFIG_TOKEN, useValue: config }, + { + provide: WrappedSocket, + useFactory: SocketFactory, + deps: [SOCKET_CONFIG_TOKEN], + }, + ]); +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/socket-io/socket-io.service.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/socket-io/socket-io.service.ts new file mode 100644 index 000000000..ff5c9ad83 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/socket-io/socket-io.service.ts @@ -0,0 +1,170 @@ +import { Observable } from 'rxjs'; +import { share } from 'rxjs/operators'; + +import * as io from 'socket.io-client'; + +import { SocketIoConfig } from './config/socket-io.config'; + +/* The `WrappedSocket` class provides a wrapper for socket.io functionality, allowing for +event handling and communication with a server. */ +export class WrappedSocket { + subscribersCounter: Record = {}; + eventObservables$: Record> = {}; + ioSocket: any; + emptyConfig: SocketIoConfig = { + rootUrl: '', + options: {}, + }; + + constructor(private readonly _config: SocketIoConfig) { + if (this._config === undefined) { + this._config = this.emptyConfig; + } + const rootUrl: string = this._config.rootUrl; + const options: any = this._config.options; + const ioFunc = (io as any).default ? (io as any).default : io; + this.ioSocket = ioFunc(rootUrl, options); + } + + /** + * The `connect` function returns the connection status of the `ioSocket`. + * @returns The `connect()` method is being called on the `ioSocket` object and the result of that + * method call is being returned. + */ + connect() { + return this.ioSocket.connect(); + } + + /** + * The function disconnects the socket connection with an optional parameter to close the connection. + * @param {boolean} [close] - The `close` parameter in the `disconnect` method is a boolean value that + * indicates whether the socket should be closed after disconnecting. If `close` is set to `true`, the + * socket will be closed after disconnecting. If `close` is not provided or set to `false`, the + * @returns The `disconnect` method is being called on `this.ioSocket` with the `close` parameter, and + * the result of this method call is being returned. + */ + disconnect(close?: boolean) { + return this.ioSocket.disconnect(close); + } + + /** + * The function sets up a new namespace for socket.io communication. + * @param {string} namespace - The `namespace` parameter is a string that represents the namespace for + * which a new socket connection will be created. + */ + of(namespace: string) { + this.ioSocket.of(namespace); + } + + /** + * The function "on" sets up an event listener with a specified event name and callback + * function. + * @param {string} eventName - The `eventName` parameter is a string that represents the name of the + * event that the callback function should listen for. + * @param callback - The `callback` parameter is a function that takes any number of arguments of any + * type and does not return any value (`void`). It is used to handle events when the specified + * `eventName` occurs. + */ + on(eventName: string, callback: (...args: any[]) => void) { + this.ioSocket.on(eventName, callback); + } + + /** + * The `fromEvent` function in TypeScript creates an Observable for a specified event name, managing + * event listeners and subscribers. + * @param {string} eventName - The `eventName` parameter is a string that represents the name of the + * event for which you want to create an Observable. This function sets up an Observable that listens + * for events with the specified name and emits the data associated with those events. + * @returns The `fromEvent` method returns an Observable of type `T` for the specified event name. + */ + fromEvent(eventName: string): Observable { + if (!this.subscribersCounter[eventName]) { + this.subscribersCounter[eventName] = 0; + } + this.subscribersCounter[eventName]++; + + if (!this.eventObservables$[eventName]) { + this.eventObservables$[eventName] = new Observable((observer: any) => { + const listener = (data: T) => { + observer.next(data); + }; + this.ioSocket.on(eventName, listener); + return () => { + this.subscribersCounter[eventName]--; + if (this.subscribersCounter[eventName] === 0) { + this.ioSocket.removeListener(eventName, listener); + delete this.eventObservables$[eventName]; + } + }; + }).pipe(share()); + } + return this.eventObservables$[eventName]; + } + + /** + * The function `fromOneTimeEvent` returns a promise that resolves with the value emitted once for + * the specified event name. + * @param {string} eventName - The `eventName` parameter is a string that represents the name of the + * event that you want to listen for and handle only once. + * @returns A Promise of type T is being returned. + */ + fromOneTimeEvent(eventName: string): Promise { + return new Promise((resolve) => this.once(eventName, resolve)); + } + + /** + * The `once` function registers a callback to be executed once for a specific event. + * @param {string} eventName - The `eventName` parameter is a string that represents the name of the + * event that the callback function should listen for. When this event occurs, the callback function + * will be executed. + * @param callback - The `callback` parameter is a function that takes any number of arguments of any + * type and does not return anything (`void`). It is a function that will be executed only once when + * the specified `eventName` occurs. + */ + once(eventName: string, callback: (...args: any[]) => void) { + this.ioSocket.once(eventName, callback); + } + + /** + * The function `emit` in TypeScript emits an event with a specified name and arguments using + * `ioSocket`. + * @param {string} eventName - The `eventName` parameter is a string that represents the name of the + * event that you want to emit using the `emit` method. This event name is used to identify the type + * of event being triggered or emitted. + * @param {any[]} args - The `args` parameter in the `emit` function represents a variable number of + * arguments that can be passed to the event handler when the event is emitted. These arguments can + * be of any type, as indicated by the `any[]` type annotation. When calling the `emit` function, you + * can + * @returns The `emit` method is returning the result of calling the `emit` method on `this.ioSocket` + * with the provided `eventName` and `args`. + */ + emit(eventName: string, ...args: any[]) { + return this.ioSocket.emit(eventName, ...args); + } + + /** + * The function `removeListener` removes a callback function from the event listener for a specific + * event name. + * @param {string} eventName - The `eventName` parameter is a string that specifies the name of the + * event for which the listener should be removed. + * @param [callback] - The `callback` parameter is a function that will be removed as a listener for + * the specified `eventName`. It is optional, meaning you can choose to provide a specific callback + * function to remove, or if omitted, all listeners for the `eventName` will be removed. + * @returns The `removeListener` method is returning the result of calling + * `this.ioSocket.removeListener(eventName, callback)`. + */ + removeListener(eventName: string, callback?: (...args: any[]) => void) { + return this.ioSocket.removeListener(eventName, callback); + } + + /** + * The function `removeAllListeners` removes all event listeners for a specific event name. + * @param {string} [eventName] - The `eventName` parameter is a string that represents the name of + * the event for which all listeners should be removed. + * @returns The `removeAllListeners` method is returning the result of calling `removeAllListeners` + * on the `ioSocket` object with the provided `eventName` parameter. + */ + removeAllListeners(eventName?: string) { + return this.ioSocket.removeAllListeners(eventName); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/usecases/index.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/usecases/index.ts new file mode 100644 index 000000000..32547aac4 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/usecases/index.ts @@ -0,0 +1 @@ +export * from './useRandomUser'; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/usecases/useRandomUser.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/usecases/useRandomUser.ts new file mode 100644 index 000000000..638b3ff60 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/usecases/useRandomUser.ts @@ -0,0 +1,18 @@ +import { RandomUserService } from '@core/services'; +import { map } from 'rxjs'; +import { plainToInstance } from 'class-transformer'; +import { RandomUserEntity } from '@core/entities'; +import { inject } from '@angular/core'; + +/** + * The `UseRandomUser` is responsible for fetching random users from the API. + * It uses the `RandomUserService` to fetch the data and then maps the response to an array of `RandomUserEntity`. + * This is an Exmaple of a usecase. + */ +export class UseRandomUser { + private readonly _randomUserService: RandomUserService = inject(RandomUserService); + + getAllUsers() { + return this._randomUserService.find().pipe(map((response) => response.map((user: any) => plainToInstance(RandomUserEntity, user)))); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/utils/arrays.utility.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/utils/arrays.utility.ts new file mode 100644 index 000000000..4123f927d --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/utils/arrays.utility.ts @@ -0,0 +1,132 @@ +/** + * + * A function that moves an item in an array to a new position. + * + * @param workArray + * @param fromIndex + * @param toIndex + */ +import { humanize } from '@core/utils/humanize-slug.utility'; +import { TranslateService } from '@ngx-translate/core'; +import { Logger } from '../services/misc/logger.service'; + +const log = new Logger('Array Utility'); + +/** + * The function `MoveItemInArray` repositions an item within an array from a specified index to another + * index. + * @param {T[]} workArray - An array of type T that you want to manipulate. + * @param {number} fromIndex - The `fromIndex` parameter represents the index of the item you want to + * move within the array. + * @param {number} toIndex - The `toIndex` parameter represents the index where you want to move the + * item to in the array. + * @returns The function `MoveItemInArray` returns the updated array after moving an item from one + * index to another within the array. + * @example MoveItemInArray([1, 2, 3, 4, 5], 1, 3) => [1, 3, 4, 2, 5] + */ +export function MoveItemInArray(workArray: T[], fromIndex: number, toIndex: number): T[] { + if (toIndex === fromIndex) { + return workArray; + } + const target = workArray[fromIndex]; + const increment = toIndex < fromIndex ? -1 : 1; + + for (let k = fromIndex; k !== toIndex; k += increment) { + workArray[k] = workArray[k + increment]; + } + workArray[toIndex] = target; + return workArray; +} + +/** + * The function `BeautifyArrayOfNumbers` takes an input, parses it into an array of numbers, and + * returns the beautified array. + * @param {any} input - The `BeautifyArrayOfNumbers` function takes an `input` parameter, which can be + * either a string or an array. If the `input` is a string, the function attempts to parse it into an + * array of numbers. If the `input` is already an array, it directly operates + * @returns The `BeautifyArrayOfNumbers` function returns an array of numbers after processing the + * input. + * @example BeautifyArrayOfNumbers('1, 2, 3, 4, 5') => [1, 2, 3, 4, 5], BeautifyArrayOfNumbers([1, '2', 3, '4', 5]) => [1, 2, 3, 4, 5] + */ +export function BeautifyArrayOfNumbers(input: any): number[] { + let array: any[]; + + if (typeof input === 'string') { + try { + const parsed = JSON.parse(input); + // Ensure the parsed result is an array + array = Array.isArray(parsed) ? parsed : [parsed]; + } catch (error) { + log.error('Error parsing input string:', error); + return []; + } + } else if (Array.isArray(input)) { + array = input; + } else { + // If the input is neither an array nor a string, log an error + log.error('Input is not an array or a string:', input); + return []; + } + + return array.map((item) => { + if (typeof item === 'string') { + try { + // This handles stringifies numbers + return Number(JSON.parse(item)); + } catch (error) { + // If parsing fails, convert directly to Number + return Number(item); + } + } + return Number(item); + }); +} + +/** + * The function EnumToStringArray converts an enum object into an array of string values or keys based + * on the specified parameter. + * @param {T} enumObj - The `enumObj` parameter in the `EnumToStringArray` function is the enum object + * that you want to convert to an array of strings. + * @param [keyArray=false] - The `keyArray` parameter in the `EnumToStringArray` function is a boolean + * flag that determines whether the function should return an array of enum keys or an array of enum + * values. If `keyArray` is set to `true`, the function will return an array of enum keys. If ` + * @returns The function `EnumToStringArray` returns an array of string values from the provided enum + * object. If the `keyArray` parameter is set to `true`, it returns an array of enum keys as strings. + * If `keyArray` is set to `false` or not provided, it returns an array of enum values as strings. + * @example EnumToStringArray({ A: 1, B: 2, C: 3 }) => ['A', 'B', 'C'], EnumToStringArray({ A: 1, B: 2, C: 3 }, true) => ['A', 'B', 'C'] + */ +export function EnumToStringArray(enumObj: T, keyArray = false): string[] { + if (keyArray) { + return Object.keys(enumObj).filter((key) => isNaN(Number(key))); + } else { + return Object.keys(enumObj) + .filter((key) => isNaN(Number(key))) + .map((key) => enumObj[key]); + } +} + +/** + * The function EnumToKeyValueArray converts an enum object into an array of key-value pairs with + * optional translation and key case splitting. + * @param {T} enumObj - The `enumObj` parameter in the `EnumToKeyValueArray` function is expected to be + * an enum object in TypeScript. It will be used to generate a key-value array where the keys are the + * string representations of the enum keys and the values are the corresponding enum values. + * @param {TranslateService} [translateService] - The `translateService` parameter is an optional + * parameter of type `TranslateService`. It is used for translating the keys of the enum into + * human-readable format before returning the key-value array. If provided, the + * `translateService.instant()` method is used to translate the keys. If not provided, the keys are + * returned as is. + * @param [keyCaseSpilt=true] - The `keyCaseSpilt` parameter in the `EnumToKeyValueArray` function is a + * boolean flag that determines whether the keys of the enum should be split based on casing. If + * `keyCaseSpilt` is set to `true`, the keys will be split based on casing before being translated + * @returns The function `EnumToKeyValueArray` returns an array of objects with `key` and `value` + * properties. The `key` property is a string representing the key of the enum object, potentially + * translated using a `TranslateService` if provided, and humanized if `keyCaseSpilt` is true. The + * `value` property is the corresponding value of the enum object. + * @example EnumToKeyValueArray({ A: 1, B: 2, C: 3 }) => [{ key: 'A', value: 1 }, { key: 'B', value: 2 }, { key: 'C', value: 3 }] + */ +export function EnumToKeyValueArray(enumObj: T, translateService?: TranslateService, keyCaseSpilt = true): { key: string; value: string }[] { + return Object.keys(enumObj) + .filter((key) => isNaN(Number(key))) + .map((key) => ({ key: translateService ? translateService.instant(humanize(key, keyCaseSpilt)) : humanize(key, keyCaseSpilt), value: enumObj[key] })); +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/utils/date-time.utility.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/utils/date-time.utility.ts new file mode 100644 index 000000000..e61977016 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/utils/date-time.utility.ts @@ -0,0 +1,286 @@ +import { TranslateService } from '@ngx-translate/core'; + +export class DateTimeUtility { + /** + * The function `getYears` returns an array of numbers representing the years between a start year and + * an end year. + * @param {number} startYear - The start year is the first year in the range of years you want to + * generate. + * @param {number} endYear - The `endYear` parameter represents the last year in the range of years you + * want to generate. + * @returns an array of numbers representing the years between the startYear and endYear (inclusive). + */ + static getYears(startYear: number, endYear: number): number[] { + const years = []; + for (let i = startYear; i <= endYear; i++) { + years.push(i); + } + return years; + } + + /** + * The function `getMonths` returns an array of objects containing the names and numbers of the months, + * with the option to use language-specific month names if available. + * @returns The function `getMonths()` returns an array of objects, where each object represents a + * month. Each object has two properties: `name` and `number`. The `name` property represents the name + * of the month, and the `number` property represents the number of the month (1 for January, 2 for + * February, and so on). + */ + static getMonths(translateService?: TranslateService): { name: string; number: number }[] { + const months = [ + { + name: translateService ? translateService.instant('calendar.' + 'all') : 'All', + number: 0, + }, + ]; + for (let i = 0; i < 12; i++) { + const name = new Date(0, i).toLocaleString('en', { month: 'long' }); + const txName = translateService ? translateService.instant('calendar.' + name.toLowerCase()) : name; + months.push({ + name: txName, + number: i + 1, + }); + } + return months; + } + + /** + * The function `getDays` returns an array of numbers representing the days of a given month and year, + * with the current day marked as "Current". + * @param translateService - The `translateService` parameter is an instance of the TranslateService required to translate the day names. + * @param {number} [month] - The `month` parameter is an optional number that represents the month. If + * not provided, it defaults to the current month. + * @param {number} [year] - The `year` parameter is an optional number that represents the year for + * which you want to get the days. If no value is provided, it defaults to the current year. + * @param keyValue - The `keyValue` parameter is an optional boolean that determines whether the array returned is number or key value pair. + * @returns an array of numbers or strings representing the days of the specified month and year. + */ + static getDays(translateService: TranslateService, month?: number, year?: number, keyValue = true): (number | string | { name: string; number: number })[] { + const now = new Date(); + month = month ?? now.getMonth(); // 0-indexed + year = year ?? now.getFullYear(); + + const daysInMonth = new Date(year, month + 1, 0).getDate(); + const daysArray: (number | string | { name: string; number: number })[] = [ + { + name: translateService ? translateService.instant('calendar.' + 'all') : 'All', + number: 0, + }, + ]; + + for (let i = 1; i <= daysInMonth; i++) { + const isToday = i === now.getDate() && month === now.getMonth() && year === now.getFullYear(); + + if (keyValue) { + const dayObj = { + name: isToday ? translateService.instant('calendar.' + 'current') : i.toString().toLowerCase(), + number: i, + }; + daysArray.push(dayObj); + } else { + if (isToday) { + daysArray.push(translateService.instant('calendar.' + 'current')); + } else { + daysArray.push(i); + } + } + } + return daysArray; + } + + /** + * The function `getWeekDays` returns an array of objects containing the names and numbers of the days + * @param translateService + */ + static getWeekDays(translateService: TranslateService): { name: string; number: number }[] { + const daysArray: { name: string; number: number }[] = []; + const days = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']; + for (let i = 0; i < days.length; i++) { + const dayObj = { + name: translateService.instant('calendar.' + days[i]), + number: i, + }; + daysArray.push(dayObj); + } + return daysArray; + } + + /** + * The function `getDay` returns the day of the month for a given date. + * @param date + * @param translateService + */ + static getDay(date: string, translateService?: TranslateService): string { + const dateObj = new Date(date); + const day = dateObj.getDate(); + if (translateService) { + return translateService.instant('calendar.' + day.toString().toLowerCase()); + } + return `${day}`; + } + + /** + * The function `getMonth` returns the month name for a given date. + * @param date + * @param translateService + */ + static getMonth(date: string, translateService?: TranslateService): string { + const dateObj = new Date(date); + const month = dateObj.getMonth(); + const monthName = new Date(0, month).toLocaleString('en', { month: 'long' }); + if (translateService) { + return translateService.instant('calendar.' + monthName.toLowerCase()); + } + return monthName; + } + + /** + * The function `getMonthNumber` returns the month number for a given date. + * @param date + */ + static getMonthNumber(date: string): number { + const dateObj = new Date(date); + return dateObj.getMonth() + 1; + } + + /** + * The function `getDateOnly` returns the date part of a given date string. + * @param date + */ + static getDateOnly(date: string): string { + const dateObj = new Date(date); + return dateObj.toISOString().split('T')[0]; + } + + /** + * The function `getRemainingTime` returns the remaining time between the current time and a given end date. + * @param endDateStr + * @param translateService + */ + static getRemainingTime(endDateStr: string, translateService?: TranslateService): string { + const endDate = new Date(endDateStr); + const currentTime = new Date(); + const remainingTime = endDate.getTime() - currentTime.getTime(); + + if (remainingTime <= 0) { + // Assuming 'Expired' or its equivalent is also defined within your translations + return translateService ? translateService.instant('dates&filters.expired') : 'Expired'; + } + + // Helper function to translate labels + const translateLabel = (label: string, count: number): string => { + if (translateService) { + const translationKey = `dates&filters.${label}${count === 1 ? '' : 's'}`; // Adjusted to match your object structure + return `${count} ${translateService.instant(translationKey)}`; + } else { + return `${count} ${label}${count === 1 ? '' : 's'}`; + } + }; + + const millisecondsPerMinute = 1000 * 60; + const millisecondsPerHour = millisecondsPerMinute * 60; + const millisecondsPerDay = millisecondsPerHour * 24; + const millisecondsPerMonth = millisecondsPerDay * 30; + const millisecondsPerYear = millisecondsPerDay * 365; + + const years = Math.floor(remainingTime / millisecondsPerYear); + if (years > 0) { + return translateLabel('year', years); + } + + const months = Math.floor(remainingTime / millisecondsPerMonth); + if (months > 0) { + return translateLabel('month', months); + } + + const days = Math.floor(remainingTime / millisecondsPerDay); + if (days > 0) { + return translateLabel('day', days); + } + + const hours = Math.floor(remainingTime / millisecondsPerHour); + if (hours > 0) { + return translateLabel('hour', hours); + } + + const minutes = Math.floor(remainingTime / millisecondsPerMinute); + return translateLabel('minute', minutes); + } + + /** + * The function `getTimeAgo` returns a string indicating the time elapsed since a given date. + * @param date + * @param translateService + */ + static getTimeAgo(date: string, translateService?: TranslateService): string { + const createdAtDate = new Date(date); + const now = new Date(); + + // Calculate the difference in milliseconds + const differenceMs = now.getTime() - createdAtDate.getTime(); + + // Check if the timestamp is in the future + if (differenceMs < 0) { + return translateService ? translateService.instant('in the future') : 'in the future'; + } + + // Calculate the difference in minutes + const differenceMinutes = Math.round(differenceMs / (1000 * 60)); + + // Helper function to translate time units + const translate = (key: string, value: string | number) => (translateService ? `${translateService.instant(key, { value })}` : `${value} ${key}`); + + // Return a string indicating the time elapsed + if (differenceMinutes < 1) { + return translate('just now', ''); + } else if (differenceMinutes < 60) { + return translate('m ago', differenceMinutes); + } else if (differenceMinutes < 1440) { + const hours = Math.floor(differenceMinutes / 60); + return translate('h ago', hours); + } else if (differenceMinutes < 10080) { + const days = Math.floor(differenceMinutes / 1440); + return translate('d ago', days); + } else if (differenceMinutes < 43800) { + const weeks = Math.floor(differenceMinutes / 10080); + return translate('w ago', weeks); + } else { + const months = Math.floor(differenceMinutes / 43800); + return translate('mo ago', months); + } + } + + /** + * Converts a date string to a UTC Date object at midnight. + * @param {string} dateStr - The date string to convert. + * @returns {Date} - A Date object representing midnight UTC of the given date. + */ + static formatDateToUTC(dateStr: string): Date { + const date = new Date(dateStr); + date.setUTCHours(0, 0, 0, 0); // Set hours, minutes, seconds, and milliseconds to zero in UTC. + return date; + } + + /** + * Converts a date object to a UTC string in the format 'YYYYMMDDTHHMMSSZ'. + * @param {string} dateStr + * @returns {string} + */ + static formatDateToUTCString(dateStr: string): string { + const date = new Date(dateStr + 'T00:00:00Z'); // Append time part to ensure it's interpreted as UTC + return date.toISOString().replace(/[-:]/g, '').slice(0, 15) + 'Z'; // Formats as YYYYMMDDTHHMMSSZ + } + + /** + * The function `isDateToday` checks if a given date string represents today's date. + * @param {string} date - The `date` parameter in the `isDateToday` function is a string representing + * a date in a specific format. The function compares this date with the current date to determine if + * it is today's date. + * @returns A boolean value indicating whether the input date string represents today's date or not. + */ + static isDateToday(date: string): boolean { + const today = new Date(); + const dateObj = new Date(date); + return today.toDateString() === dateObj.toDateString(); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/utils/form-validators.utility.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/utils/form-validators.utility.ts new file mode 100644 index 000000000..08339c0fc --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/utils/form-validators.utility.ts @@ -0,0 +1,179 @@ +import { AbstractControl, FormArray, FormControl, FormGroup, ValidationErrors, ValidatorFn } from '@angular/forms'; + +export function arrayNotEmptyValidator(): ValidatorFn { + return (control: AbstractControl): Record | null => { + const isArray = Array.isArray(control.value); + const isEmpty = isArray && control.value.length === 0; + return isEmpty ? { arrayNotEmpty: { value: control.value } } : null; + }; +} + +export function isValidDate(dateString: string): boolean { + return /^\d{4}[-/]\d{2}[-/]\d{2}$/.test(dateString); +} + +export function validateCustomDate(control: FormControl): ValidationErrors | null { + if (!/^\d{4}[-/]\d{2}[-/]\d{2}$/.test(control.value)) { + return { invalidDateFormat: true }; + } + return null; +} + +// mustMatch validator function +export function mustMatch(controlName: string, matchingControlName: string): ValidatorFn { + return (formGroup: FormGroup): ValidationErrors | null => { + const control = formGroup.controls[controlName]; + const matchingControl = formGroup.controls[matchingControlName]; + + if (matchingControl.errors && !matchingControl.errors['mustMatch']) { + return null; + } + + // set error on matchingControl if validation fails + if (control.value !== matchingControl.value) { + matchingControl.setErrors({ mustMatch: true }); + return { mustMatch: true }; + } else { + matchingControl.setErrors(null); + return null; + } + }; +} + +/** + * Creates a validator function to validate that one time is in the future relative to another. + * @param startControlName The form control name for the start time. + * @param endControlName The form control name for the end time. + * @returns A validator function. + */ +export function timeRangeValidator(startControlName: string, endControlName: string) { + return (group: FormGroup): ValidationErrors | null => { + const startTime = group.get(startControlName)?.value; + const endTime = group.get(endControlName)?.value; + + if (startTime && endTime) { + const [startHour, startMinute] = startTime.split(':').map(Number); + const [endHour, endMinute] = endTime.split(':').map(Number); + + const startDate = new Date(1970, 0, 1, startHour, startMinute); + const endDate = new Date(1970, 0, 1, endHour, endMinute); + + if (endDate <= startDate) { + // If the end time is not after the start time, return an error object. + const error = { timeRangeInvalid: true }; + group.get(endControlName)?.setErrors(error); + return error; + } + } + + // If no errors, clear existing errors related to time range validation. + group.get(endControlName)?.setErrors(null); + return null; // Return null if no validation errors + }; +} + +/** + * Creates a generic validator function to check if one date is after another within the same FormGroup. + * @param startControlName The form control name for the start date. + * @param endControlName The form control name for the end date. + * @returns A validator function that adds an error if the end date is not after the start date. + */ +export function dateRangeValidator(startControlName: string, endControlName: string): ValidatorFn { + return (group: AbstractControl): ValidationErrors | null => { + if (!(group instanceof FormGroup)) return null; + + const startDate = group.get(startControlName)?.value; + const endDate = group.get(endControlName)?.value; + + // Check if both dates are present before comparing + if (startDate && endDate) { + const start = new Date(startDate); + const end = new Date(endDate); + + // Check if the end date is after the start date + if (end <= start) { + // If not, return an error object on the form group + const errorObject = { dateRangeInvalid: true }; + // Optionally, set errors directly on the end date control to make handling specific error messages easier + group.get(endControlName)?.setErrors(errorObject); + return errorObject; + } + } + + // If everything is fine, clear errors related to the date range from the end date control + const endControlErrors = group.get(endControlName)?.errors; + if (endControlErrors?.['dateRangeInvalid']) { + delete endControlErrors['dateRangeInvalid']; + group.get(endControlName)?.setErrors(Object.keys(endControlErrors).length ? endControlErrors : null); + } + + // Return null if no validation errors + return null; + }; +} + +/** + * Validates that one date control's value is less than another, where one control is within a FormArray. + * @param form The FormGroup containing both the FormArray and the control to compare. + * @param arrayPath The path to the FormArray in the FormGroup. + * @param arrayDateControlName The name of the control within the FormArray groups to compare. + * @param controlName The name of the control outside the FormArray to compare against the array's date control. + * @returns A validator function that applies the specified validation. + */ +export function dateRangeArrayAndControlValidator(form: FormGroup, arrayPath: string, arrayDateControlName: string, controlName: string): ValidatorFn { + return (control: AbstractControl): { [key: string]: any } | null => { + const dateArray = form?.get(arrayPath) as FormArray; + const endDateControl = control.get(controlName); + const endDate = endDateControl?.value; + + if (dateArray && dateArray.length > 0 && endDate) { + const firstEventGroup = dateArray.at(0) as FormGroup; + const startDate = firstEventGroup.get(arrayDateControlName)?.value; + + if (startDate && endDate) { + const start = new Date(startDate); + const end = new Date(endDate); + + // Set true if invalid, false otherwise + const isInvalid = end <= start; + endDateControl?.setErrors(isInvalid ? { dateRangeInvalid: true } : null); + return isInvalid ? { dateRangeInvalid: true } : null; + } + } + + // If the conditions don't hold or are valid, clear any errors related to date range + endDateControl?.setErrors(null); + return null; // If all checks pass, return null + }; +} + +/** + * Creates a validator function to ensure the uniqueness of a field within a FormArray or FormGroup. + * @param parentControl A reference to the parent FormArray or FormGroup. + * @param fieldName The name of the field within the form groups to check for uniqueness. + * @returns A validator function. + */ +export function uniqueFieldValidator(parentControl: AbstractControl, fieldName: string): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const currentValue = control.value; + let occurrences = 0; + + // Handling for FormArray + if (parentControl instanceof FormArray) { + occurrences = parentControl.controls.filter((ctrl) => ctrl.get(fieldName)?.value === currentValue).length; + } + // Handling for FormGroup + else if (parentControl instanceof FormGroup) { + const controls = Object.values(parentControl.controls); + occurrences = controls.filter((ctrl) => { + // Assuming each control is a FormGroup that might contain the fieldName + if (ctrl instanceof FormGroup) { + return ctrl.get(fieldName)?.value === currentValue; + } + return false; + }).length; + } + + return occurrences > 1 ? { uniqueField: true } : null; + }; +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/utils/humanize-slug.utility.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/utils/humanize-slug.utility.ts new file mode 100644 index 000000000..7d1a7e24b --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/utils/humanize-slug.utility.ts @@ -0,0 +1,60 @@ +/** + * The `humanize` function takes a string and optionally splits it based on case or + * specific characters, then capitalizes the first letter of each fragment and returns the humanized + * string. + * @param {string} str - The `str` parameter is a string that you want to humanize, meaning to format + * it in a more readable and user-friendly way. This function takes a string as input and transforms it + * into a human-readable format by capitalizing the first letter of each word and converting the rest + * of the letters to + * @param [caseSplit=false] - The `caseSplit` parameter in the `humanize` function determines whether + * to insert a space before all capital letters in camelCase or PascalCase strings. If `caseSplit` is + * set to `true`, the function will insert a space before each capital letter that is not the first + * character in the + * @returns The `humanize` function returns a human-readable version of the input string by + * capitalizing the first letter of each word and converting the rest of the letters to lowercase. If + * the `caseSplit` parameter is set to `true`, it also inserts a space before each capital letter in + * camelCase or PascalCase strings. + * @example humanize('helloWorld') => 'Hello World', humanize('helloWorld', true) => 'Hello World' + */ +export function humanize(str: string, caseSplit = false): string { + if (!str) return str; + + if (caseSplit) { + // Insert a space before all caps in camelCase or PascalCase strings, avoid the first character + str = str.replace(/(.)([A-Z])/g, '$1 $2'); + } + + // Determine the split pattern based on the existence of certain characters + let frags; + if (str.includes('_')) { + frags = str.split('_'); + } else if (str.includes('-')) { + frags = str.split('-'); + } else { + frags = str.split(' '); + } + + for (let i = 0; i < frags.length; i++) { + frags[i] = frags[i].charAt(0).toUpperCase() + frags[i].slice(1).toLowerCase(); + } + + return frags.join(' '); +} + +/** + * The function `deHumanize` takes a string input, converts it to lowercase, replaces spaces with a + * specified operator (default is underscore), and returns the modified string. + * @param {string} str - The `str` parameter is a string that represents the text you want to + * dehumanize. It will be converted to lowercase and have spaces replaced with the specified + * `operator`. + * @param [operator=_] - The `operator` parameter in the `deHumanize` function is used to specify the + * character that will replace the spaces in the input string. By default, it is set to an underscore + * ('_'). + * @returns The `deHumanize` function returns a string where all spaces in the input string have been + * replaced with the specified `operator` (default is underscore) after converting the input string to + * lowercase. + * @example deHumanize('Hello World') => 'hello_world', deHumanize('Hello World', '-') => 'hello-world' + */ +export function deHumanize(str: string, operator = '_') { + return str.toLowerCase().split(' ').join(operator); +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/utils/index.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/utils/index.ts new file mode 100644 index 000000000..1af79f4a4 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/utils/index.ts @@ -0,0 +1,7 @@ +export * from './arrays.utility'; +export * from './date-time.utility'; +export * from './form-validators.utility'; +export * from './humanize-slug.utility'; +export * from './local-storage-data.utility'; +export * from './mark-invalid-form-controls.utility'; +export * from './objects.utility'; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/utils/local-storage-data.utility.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/utils/local-storage-data.utility.ts new file mode 100644 index 000000000..70b0726a8 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/utils/local-storage-data.utility.ts @@ -0,0 +1,79 @@ +import { Credentials } from '@core/entities'; + +/** + * List of local storage keys + */ +export enum LocalStorageKeys { + TOKEN = 'token', + REFRESH_TOKEN = 'refreshToken', + USER_ID = 'id', + CREDENTIALS = 'credentials', +} + +// const prefix = '@app:'; +// const prodEnv = environment.production; +// const secretKey = 'a-quick-brown-fox-jumps-over-the-lazy-dog'; +// const secretKeyName = '@entry'; +// const secretKeyValue = 'xx1029384756'; + +export const GetCredentials = () => getDataFromLocalStorage(LocalStorageKeys.CREDENTIALS) as Credentials; + +export const SetCredentials = (credentials: Credentials) => saveDataToLocalStorage(LocalStorageKeys.CREDENTIALS, credentials); + +export const GetToken = () => getDataFromLocalStorage(LocalStorageKeys.CREDENTIALS)[LocalStorageKeys.TOKEN]; + +export const GetRefreshToken = () => getDataFromLocalStorage(LocalStorageKeys.CREDENTIALS)[LocalStorageKeys.REFRESH_TOKEN]; + +export const GetUserId = () => getDataFromLocalStorage(LocalStorageKeys.CREDENTIALS)[LocalStorageKeys.USER_ID]; + +export const ClearStorage = () => clearStorage(); + +export const RemoveAuthData = () => { + for (const key in LocalStorageKeys) { + removeDataFromLocalStorage(key); + } +}; + +// const xorWithSecretKey = (text: string, secret: string): string => { +// let result = ''; +// for (let i = 0; i < text.length; i++) { +// result += String.fromCharCode(text.charCodeAt(i) ^ secret.charCodeAt(i % secret.length)); +// } +// return result; +// }; + +const saveDataToLocalStorage = (key: any, value: any) => { + // if (prodEnv) { + // key = btoa(prefix + key.toString()); // Encode key as base64 + // key = xorWithSecretKey(key, secretKey); // XOR the key with the secret key + // value = btoa(JSON.stringify(value)); // Encode value as base64 + // value = xorWithSecretKey(value, secretKey); // XOR the value with the secret key + // localStorage.setItem(secretKeyName, secretKeyValue); + // } + localStorage.setItem(key, JSON.stringify(value)); +}; + +const getDataFromLocalStorage = (key: any) => { + // const isSecretKey = localStorage.getItem(secretKeyName) === secretKeyValue; + // if (prodEnv && isSecretKey) { + // key = btoa(prefix + key.toString()); // Encode key as base64 + // key = xorWithSecretKey(key, secretKey); // XOR the key with the secret key + // const data = localStorage.getItem(key); + // if (!data) window.location.href = '/logout'; + // const decodedValue = xorWithSecretKey(data, secretKey); // XOR the value with the secret key + // return JSON.parse(atob(decodedValue)); // Decode base64 value + // } + const data = localStorage.getItem(key); + if (!data) window.location.href = '/logout'; + return JSON.parse(data); +}; + +const removeDataFromLocalStorage = (key: any) => { + // if (prodEnv) { + // key = btoa(prefix + key.toString()); // Encode key as base64 + // key = xorWithSecretKey(key, secretKey); // XOR the key with the secret key + // } + localStorage.removeItem(key); +}; + +const clearStorage = () => localStorage.clear(); diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/utils/mark-invalid-form-controls.utility.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/utils/mark-invalid-form-controls.utility.ts new file mode 100644 index 000000000..b03b72ed6 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/utils/mark-invalid-form-controls.utility.ts @@ -0,0 +1,57 @@ +/** + * This function recursively marks all form controls as dirty and touched, and scrolls to the first + * invalid control if provided with a reference element. + * @param {T} control - The `control` parameter in the `markInvalidFormControls` function represents a + * form control element in Angular forms. It can be of type `FormGroup`, `FormArray`, or `FormControl`. + * The function recursively marks all form controls as dirty and touched, and scrolls to the first + * invalid control if an + * @param {ElementRef} [elRef] - The `elRef` parameter in the `markInvalidFormControls` function is an + * optional parameter of type `ElementRef`. It is used to reference an element in the DOM, typically + * for scrolling purposes when marking form controls as invalid. If provided, the function will scroll + * to the first invalid control using + */ + +import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms'; +import { ElementRef } from '@angular/core'; + +export function markInvalidFormControls(control: T, elRef?: ElementRef): void { + if (control instanceof FormGroup) { + const controls = control.controls; + control.markAllAsTouched(); + Object.keys(controls).forEach((key) => { + controls[key].markAsDirty(); + if (elRef) { + markInvalidFormControls(controls[key], elRef); + } else { + markInvalidFormControls(controls[key]); + } + }); + if (elRef) { + scrollToFirstInvalidControl(elRef); + } + } else if (control instanceof FormArray) { + control.controls.forEach((formControl) => { + formControl.markAsDirty(); + formControl.markAsTouched(); + }); + if (elRef) { + scrollToFirstInvalidControl(elRef); + } + } else if (control instanceof FormControl) { + control.markAsDirty(); + control.markAsTouched(); + if (elRef) { + scrollToFirstInvalidControl(elRef); + } + } else { + throw new Error('Error: unexpected control value'); + } +} + +function scrollToFirstInvalidControl(refElement: ElementRef) { + const firstInvalidControl: HTMLElement = refElement.nativeElement.querySelector('form .ng-invalid'); + if (firstInvalidControl) { + firstInvalidControl.scrollIntoView({ block: 'start', behavior: 'smooth' }); + (firstInvalidControl as HTMLElement).focus(); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/utils/objects.utility.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/utils/objects.utility.ts new file mode 100644 index 000000000..8856fabe3 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/@core/utils/objects.utility.ts @@ -0,0 +1,71 @@ +/** + * The function removes null, undefined, and empty properties from an object recursively, + * except for the properties specified in the whitelist. + * @param {any} object - The input object from which you want to remove null, undefined, and empty properties. + * @param {string[]} [whitelist=[]] - An optional array of property names that should not be deleted if empty. + * @returns An object with null, undefined, and empty properties removed, except for the properties in the whitelist. + */ +export function removeNullAndUndefinedProperties(object: any, whitelist: string[] = []): any { + // Check if the input is null, undefined, or an empty string, return null for those cases + if (object === null || object === undefined || object === '') { + return null; + } + + // Handle arrays: map over items, remove unwanted values, and filter out nulls after the transformation + if (Array.isArray(object)) { + return object.map((item) => removeNullAndUndefinedProperties(item, whitelist)).filter((item) => item !== null && item !== undefined && item !== ''); + } + + // For objects: create a new object, recursively remove unwanted values, and include properties that aren't null, undefined, or an empty string + if (typeof object === 'object') { + const result: any = {}; + for (const key of Object.keys(object)) { + const value = removeNullAndUndefinedProperties(object[key], whitelist); + if (whitelist.includes(key) || (value !== null && value !== undefined && value !== '')) { + result[key] = value; + } + } + return result; + } + + // Return the object for non-object and non-array types that aren't null, undefined, or an empty string + return object; +} + +/** + * Function to sort object keys + * @param obj as Object + * @returns Object + */ + +export function sortObjectByKeys(obj: any) { + return Object.keys(obj) + .sort() + .reduce((accumulator, key) => { + accumulator[key] = obj[key]; + return accumulator; + }, {}); +} + +/** + * A function that takes an object and an array of properties to exclude and returns a new object + * + * @param object + * @param excludeKeys + * @returns new object without excluded keys + */ + +export function OmitProperties(object: NonNullable, excludeKeys: Set): NonNullable { + const filteredPairs = Object.entries(object).filter(([key]) => !excludeKeys.has(key)); + return Object.fromEntries(filteredPairs); +} + +/** + * A function that takes an object and an array of properties to include and returns a new object + * + * @returns new object with only included keys + * @param obj + */ +export function checkIfObjectIsEmpty(obj: any): boolean { + return Object.keys(obj).length === 0; +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/app.component.scss b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/app.component.scss new file mode 100644 index 000000000..5f2e95cfa --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/app.component.scss @@ -0,0 +1,27 @@ +button { + background-color: #4caf50; + border: none; + color: white; + padding: 15px 32px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + margin: 4px 2px; + cursor: pointer; +} + +body { + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Roboto, + Helvetica, + Arial, + sans-serif, + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol"; +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/app.component.spec.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/app.component.spec.ts new file mode 100644 index 000000000..c8b180da6 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/app.component.spec.ts @@ -0,0 +1,29 @@ +import { TestBed } from '@angular/core/testing'; +import { AppComponent } from './app.component'; + +describe('AppComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AppComponent], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have the 'angular-boilerplate' title`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app.title).toEqual('angular-boilerplate'); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('h1')?.textContent).toContain('Hello, angular-boilerplate'); + }); +}); diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/app.component.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/app.component.ts new file mode 100644 index 000000000..28502f976 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/app.component.ts @@ -0,0 +1,78 @@ +import { Component, OnInit, inject, ChangeDetectionStrategy, DestroyRef } from '@angular/core'; +import { NavigationEnd, Router, RouterOutlet } from '@angular/router'; +import { I18nService } from '@app/i18n'; +import { Title } from '@angular/platform-browser'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { environment } from '@env/environment'; +import { filter, merge } from 'rxjs'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { AppUpdateService, Logger } from '@core/services'; +import { SocketIoService } from '@core/socket-io'; + +@Component({ + selector: 'app-root', + imports: [RouterOutlet, TranslateModule], + template: '', + styleUrl: './app.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AppComponent implements OnInit { + private readonly router = inject(Router); + private readonly titleService = inject(Title); + private readonly translateService = inject(TranslateService); + private readonly i18nService = inject(I18nService); + private readonly socketService = inject(SocketIoService); + private readonly updateService = inject(AppUpdateService); + private readonly destroyRef = inject(DestroyRef); + + title = 'angular-boilerplate'; + + ngOnInit() { + // Setup logger + if (environment.production) { + Logger.enableProductionMode(); + } + + // Initialize i18nService with default language and supported languages + this.i18nService.init(environment.defaultLanguage, environment.supportedLanguages); + + const onNavigationEnd = this.router.events.pipe(filter((event) => event instanceof NavigationEnd)); + + merge(this.translateService.onLangChange, onNavigationEnd) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((event) => { + const titles = this.getTitle(this.router.routerState, this.router.routerState.root); + + if (titles.length === 0) { + this.titleService.setTitle(this.translateService.instant('Home')); + } else { + const translatedTitles = titles.map((titlePart) => this.translateService.instant(titlePart)); + const allTitlesSame = translatedTitles.every((title, _, arr) => title === arr[0]); + this.titleService.setTitle(allTitlesSame ? translatedTitles[0] : translatedTitles.join(' | ')); + } + + if (event['lang']) { + // Uncomment the following line to force a reload of the page when the language changes if needed for translations from backend + // window.location.reload(); + } + }); + + // Connect to Socket + this.socketService.connect(); + + // update service + this.updateService.subscribeForUpdates(); + } + + getTitle(state: any, parent: any): any[] { + const data = []; + if (parent && parent.snapshot.data && parent.snapshot.data.title) { + data.push(parent.snapshot.data.title); + } + + if (state && parent) { + data.push(...this.getTitle(state, state.firstChild(parent))); + } + return data; + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/app.config.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/app.config.ts new file mode 100644 index 000000000..65552a4de --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/app.config.ts @@ -0,0 +1,75 @@ +import { ApplicationConfig, enableProdMode, importProvidersFrom, provideZonelessChangeDetection } from '@angular/core'; +import { PreloadAllModules, provideRouter, RouteReuseStrategy, withEnabledBlockingInitialNavigation, withInMemoryScrolling, withPreloading, withRouterConfig } from '@angular/router'; +import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { provideServiceWorker } from '@angular/service-worker'; +import { TranslateModule } from '@ngx-translate/core'; +import { provideHotToastConfig } from '@ngxpert/hot-toast'; + +import { routes } from './app.routes'; +import { environment } from '@env/environment'; +import { ApiPrefixInterceptor, ErrorHandlerInterceptor } from '@core/interceptors'; +import { RouteReusableStrategy } from '@core/helpers'; +import { provideSocketIo } from '@core/socket-io'; + +if (environment.production) { + enableProdMode(); +} + +export const appConfig: ApplicationConfig = { + providers: [ + provideZonelessChangeDetection(), + + importProvidersFrom(TranslateModule.forRoot()), + + provideSocketIo({ + rootUrl: null, // TODO: provide your own socket.io server URL + options: { + transports: ['websocket'], + }, + }), + + provideServiceWorker('ngsw-worker.js', { + enabled: environment.production, + scope: '/', + registrationStrategy: 'registerWhenStable:30000', + }), + + provideRouter( + routes, + withRouterConfig({ + onSameUrlNavigation: 'reload', + paramsInheritanceStrategy: 'always', + }), + withEnabledBlockingInitialNavigation(), + withInMemoryScrolling({ + scrollPositionRestoration: 'enabled', + anchorScrolling: 'enabled', + }), + withPreloading(PreloadAllModules), + ), + + provideHotToastConfig({ + reverseOrder: true, + dismissible: true, + autoClose: true, + position: 'top-right', + theme: 'snackbar', + }), + + provideHttpClient(withInterceptorsFromDi()), + { + provide: HTTP_INTERCEPTORS, + useClass: ApiPrefixInterceptor, + multi: true, + }, + { + provide: HTTP_INTERCEPTORS, + useClass: ErrorHandlerInterceptor, + multi: true, + }, + { + provide: RouteReuseStrategy, + useClass: RouteReusableStrategy, + }, + ], +}; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/app.routes.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/app.routes.ts new file mode 100644 index 000000000..3ebf51aff --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/app.routes.ts @@ -0,0 +1,11 @@ +import { Routes } from '@angular/router'; +import { AUTH_ROUTES } from '@app/auth/auth.routing'; +import { PAGES_ROUTES } from '@pages/pages-routing.module'; + +export const routes: Routes = [ + ...AUTH_ROUTES, + ...PAGES_ROUTES, + // Fallback when no prior route is matched + { path: '', redirectTo: '/login', pathMatch: 'full' }, + { path: '**', redirectTo: '/login', pathMatch: 'full' }, +]; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/auth.routing.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/auth.routing.ts new file mode 100644 index 000000000..bb10033b1 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/auth.routing.ts @@ -0,0 +1,18 @@ +import { Routes } from '@angular/router'; +import { AlreadyLoggedCheckGuard } from '@app/auth/guard/authentication.guard'; +import { marker } from '@colsen1991/ngx-translate-extract-marker'; + +export const AUTH_ROUTES: Routes = [ + { path: '', redirectTo: '/login', pathMatch: 'full' }, + { + path: 'login', + canActivate: [AlreadyLoggedCheckGuard], + loadComponent: () => import('@app/auth/login/login.component').then((m) => m.LoginComponent), + data: { title: marker('Login') }, + }, + { + path: 'logout', + loadComponent: () => import('@app/auth/logout/logout.component').then((m) => m.LogoutComponent), + data: { title: marker('Logout') }, + }, +]; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/enums/permissions.enum.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/enums/permissions.enum.ts new file mode 100644 index 000000000..86a27d250 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/enums/permissions.enum.ts @@ -0,0 +1,28 @@ +/** + * Enum for permissions + * @readonly + * @enum {string} + * + * @description The permissions enum is used to define the permissions in the application. it is derived from the role-based access control (RBAC) model and the + * predefined role based permissions, which are defined in the `appSetting` object in the `@core/constants/app-settings` file. + * + * This permission enum can be used in routes, components, services, and directives to check if the user has the required permission to access the resource. + * + * The permission in this enum is derived as a combination of the action and the resource. For example, `CREATE_USER` means the user has permission to create a user. + * which in `appSettings` is defined as `role: { [ROLE.ADMIN]: { create: { user: true } } }`. + * + * @example + * ```typescript + * import { PERMISSIONS } from '@app/auth/enums/permissions.enum'; + * + * if (this._permissionService.hasPermission(PERMISSIONS.CREATE_USER)) { + * // do something + * } + */ + +export enum PERMISSIONS { + CREATE_USER = 'create.user', + CREATE_SALE = 'create.sale', + ACCESS_USER = 'access.user', + ACCESS_SALE = 'access.sale', +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/enums/roles.enum.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/enums/roles.enum.ts new file mode 100644 index 000000000..6256ca03d --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/enums/roles.enum.ts @@ -0,0 +1,6 @@ +export enum ROLE { + ADMIN = 'admin', + USER = 'user', + MEMBER = 'member', + GUEST = 'guest', +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/guard/authentication.guard.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/guard/authentication.guard.ts new file mode 100644 index 000000000..e67459d16 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/guard/authentication.guard.ts @@ -0,0 +1,47 @@ +import { inject, Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; + +import { Logger } from '@app/@core/services'; +import { CredentialsService } from '@app/auth'; + +const log = new Logger('AuthenticationGuard'); + +/* The `AlreadyLoggedCheckGuard` class is a guard in TypeScript that checks if a user is already +authenticated and redirects them to the dashboard if they are. */ +@Injectable({ + providedIn: 'root', +}) +export class AlreadyLoggedCheckGuard { + private readonly credentialsService = inject(CredentialsService); + private readonly router = inject(Router); + + async canActivate(): Promise { + const isAuthenticated = this.credentialsService.isAuthenticated(); + if (isAuthenticated) { + this.router.navigateByUrl('/dashboard'); + return false; + } else { + return true; + } + } +} + +/* The AuthenticationGuard class in TypeScript is used to check if a user is authenticated and redirect +to the login page if not. */ +@Injectable({ + providedIn: 'root', +}) +export class AuthenticationGuard { + private readonly router = inject(Router); + private readonly credentialsService = inject(CredentialsService); + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { + if (this.credentialsService.isAuthenticated()) { + return true; + } + + log.debug('Not authenticated, redirecting and adding redirect url...'); + this.router.navigate(['/login'], { queryParams: { redirect: state.url }, replaceUrl: true }); + return false; + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/guard/permission.guard.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/guard/permission.guard.ts new file mode 100644 index 000000000..5f3927af3 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/guard/permission.guard.ts @@ -0,0 +1,63 @@ +import { inject } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivateChildFn, CanActivateFn, Router, RouterStateSnapshot } from '@angular/router'; +import { PERMISSIONS, PermissionService, ROLE } from '@auth'; + +/** + * The `PermissionGuard` function checks for required roles and permissions before + * allowing access to a route. + * @param {ActivatedRouteSnapshot} route - The `route` parameter in the `PermissionGuard` function + * represents the current route being activated. It is of type `ActivatedRouteSnapshot` which contains + * information about the route, its parameters, data, and the URL segments. This parameter is used to + * access route data such as roles and permissions specified for + * @param {RouterStateSnapshot} state - The `state` parameter in the `PermissionGuard` function refers + * to the current RouterStateSnapshot object. It represents the state of the router at a specific point + * in time and contains information about the current route, URL, and any additional data associated + * with the route. The `state` parameter is used + * @returns The `PermissionGuard` function is returning a boolean value (`true`) if the user has the + * required roles and permissions specified in the route data. If the user does not have the required + * roles or permissions, the function will call the `handleUnauthorized` function and return the result + * of that function, which is typically a redirect to an unauthorized page or action. + */ +export const PermissionGuard: CanActivateFn & CanActivateChildFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { + const permissionService = inject(PermissionService); + const router = inject(Router); + + // Check if roles are specified in the route and validate them + const requiredRoles = route.data['roles'] as ROLE[] | undefined; + if (requiredRoles?.length && !permissionService.hasRole(requiredRoles)) { + return handleUnauthorized(router); + } + + // Check permissions + const requiredPermissions = route.data['permissions'] as PERMISSIONS[] | undefined; + if (requiredPermissions?.length) { + if (!checkPermissions(requiredPermissions, permissionService)) { + return handleUnauthorized(router); + } + } + + return true; +}; + +// Utility function to handle unauthorized access +function handleUnauthorized(router: Router): boolean { + router.navigate(['/unauthorized']); + return false; +} + +// Utility function to check permissions +function checkPermissions(permissions: PERMISSIONS[], permissionService: PermissionService): boolean { + // Just an additional layer to check for special permissions, if you dont have any ignore it + const specialPermissionHandlers = {}; + + for (const permission of permissions) { + const specialPermissionCheck = specialPermissionHandlers[permission]; + if (specialPermissionCheck) { + if (specialPermissionCheck()) return true; + } else if (permissionService.hasPermission(permission)) { + return true; + } + } + + return false; +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/index.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/index.ts new file mode 100644 index 000000000..0ee588dae --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/index.ts @@ -0,0 +1,12 @@ +export * from './auth.routing'; +export * from './login/login.component'; +export * from './guard/authentication.guard'; + +// Services +export * from './services/authentication.service'; +export * from './services/credentials.service'; +export * from './services/permissions.service'; + +// Enums +export * from './enums/permissions.enum'; +export * from './enums/roles.enum'; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/login/login.component.html b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/login/login.component.html new file mode 100644 index 000000000..0e9440807 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/login/login.component.html @@ -0,0 +1,16 @@ + diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/login/login.component.scss b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/login/login.component.scss new file mode 100644 index 000000000..7a2968c04 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/login/login.component.scss @@ -0,0 +1,79 @@ +.login { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + width: 100vw; + margin: 0; + background-color: var(--white); + position: relative; +} + +.login-box { + width: 300px; + padding: 40px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + background: #fff; + border-radius: 8px; + text-align: center; + + h2 { + margin: 0 0 20px; + font-size: 24px; + color: #333; + } + + .input-field { + position: relative; + margin-bottom: 30px; + + input { + width: 100%; + padding: 10px; + border: none; + border-bottom: 1px solid #333; + outline: none; + background: none; + font-size: 16px; + color: #333; + + &:focus + label, + &:not(:placeholder-shown) + label { + top: -14px; + font-size: 12px; + color: #333; + } + } + + label { + position: absolute; + left: 0; + top: 10px; + font-size: 16px; + color: #aaa; + transition: all 0.3s; + } + } + + button { + width: 100%; + padding: 10px; + border: none; + border-radius: 4px; + font-size: 16px; + color: #fff; + background-color: #333; + cursor: pointer; + transition: background-color 0.3s; + + &:hover { + background-color: #555; + } + } +} + +.lang-selector { + position: absolute; + bottom: 40px; + right: 40px; +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/login/login.component.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/login/login.component.ts new file mode 100644 index 000000000..2a8833ec5 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/login/login.component.ts @@ -0,0 +1,53 @@ +import { Component, ChangeDetectionStrategy, inject, DestroyRef } from '@angular/core'; +import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { environment } from '@env/environment'; +import { AuthenticationService } from '@app/auth/services/authentication.service'; + +@Component({ + selector: 'app-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'], + imports: [ReactiveFormsModule], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LoginComponent { + private readonly router = inject(Router); + private readonly route = inject(ActivatedRoute); + private readonly authService = inject(AuthenticationService); + private readonly fb = inject(FormBuilder); + private readonly destroyRef = inject(DestroyRef); + + version: string | null = environment.version; + + loginForm = this.fb.nonNullable.group({ + username: ['', Validators.required], + password: ['', Validators.required], + }); + + login() { + if (this.loginForm.invalid) { + return; + } + + const { username, password } = this.loginForm.getRawValue(); + + this.authService + .login({ username, password }) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (res) => { + if (res) { + console.log('Login successful'); + this.router.navigate([this.route.snapshot.queryParams['redirect'] || '/dashboard'], { replaceUrl: true }).then(() => { + console.log('Navigated to dashboard'); + }); + } + }, + error: (error) => { + // Handle the error here + }, + }); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/logout/logout.component.html b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/logout/logout.component.html new file mode 100644 index 000000000..240e2b4d4 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/logout/logout.component.html @@ -0,0 +1 @@ +

Logged Out!

diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/logout/logout.component.scss b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/logout/logout.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/logout/logout.component.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/logout/logout.component.ts new file mode 100644 index 000000000..744105910 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/logout/logout.component.ts @@ -0,0 +1,37 @@ +import { Component, OnInit, ChangeDetectionStrategy, inject } from '@angular/core'; +import { Router } from '@angular/router'; +import { AuthenticationService } from '@app/auth/services/authentication.service'; +import { CredentialsService } from '@app/auth/services/credentials.service'; + +@Component({ + selector: 'app-logout', + templateUrl: './logout.component.html', + styleUrls: ['./logout.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LogoutComponent implements OnInit { + private readonly authService = inject(AuthenticationService); + private readonly router = inject(Router); + private readonly credentialsService = inject(CredentialsService); + + ngOnInit() { + if (!this.credentialsService.isAuthenticated()) { + this.credentialsService.setCredentials(); + this.router.navigate(['/login']).then(() => { + window.location.reload(); + }); + } else { + this.authService.logout().subscribe({ + next: () => { + this.credentialsService.setCredentials(); + this.router.navigate(['/login']).then(() => { + window.location.reload(); + }); + }, + error: () => { + console.error('Error logging out'); + }, + }); + } + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/services/authentication.service.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/services/authentication.service.ts new file mode 100644 index 000000000..822f7ac77 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/services/authentication.service.ts @@ -0,0 +1,91 @@ +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; + +import { CredentialsService } from '@app/auth'; +import { Credentials } from '@core/entities'; + +export interface LoginContext { + username: string; + password: string; + remember?: boolean; + isMobile?: boolean; +} + +/** + * Provides a base for authentication workflow. + * The login/logout methods should be replaced with proper implementation. + */ +@Injectable({ + providedIn: 'root', +}) +export class AuthenticationService { + constructor(private readonly _credentialsService: CredentialsService) {} + + /** + * Authenticates the user. + * @param context The login parameters. + * @return The user credentials. + */ + login(context: LoginContext): Observable { + // Parse name from username (e.g., "johnsmith" -> "John Smith") + const { firstName, lastName } = this.parseUsername(context.username); + + const credentials: Credentials = new Credentials({ + username: context.username, + id: crypto.randomUUID(), + token: '123456', + refreshToken: '123456', + expiresIn: 3600, + roles: ['admin'], + email: `${context.username}@example.com`, + firstName, + lastName, + }); + this._credentialsService.setCredentials(credentials, context.remember); + + return of(credentials); + } + + /** + * Parse username to extract first and last name. + * Handles formats like: "johnsmith", "john.smith", "john_smith", "JohnSmith" + */ + private parseUsername(username: string): { firstName: string; lastName: string } { + // Check for separators (. or _) + if (username.includes('.') || username.includes('_')) { + const parts = username.split(/[._]/); + return { + firstName: this.capitalize(parts[0]), + lastName: parts.length > 1 ? this.capitalize(parts[1]) : '', + }; + } + + // Check for camelCase (e.g., "JohnSmith" or "johnSmith") + const camelCaseMatch = username.match(/^([a-z]+)([A-Z][a-z]+)$/i); + if (camelCaseMatch) { + return { + firstName: this.capitalize(camelCaseMatch[1]), + lastName: this.capitalize(camelCaseMatch[2]), + }; + } + + // Default: use entire username as first name + return { + firstName: this.capitalize(username), + lastName: '', + }; + } + + private capitalize(str: string): string { + if (!str) return ''; + return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); + } + + /** + * Logs out the user and clear credentials. + * @return True if the user was logged out successfully. + */ + logout(): Observable { + return of(true); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/services/credentials.service.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/services/credentials.service.ts new file mode 100644 index 000000000..b1cd019d0 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/services/credentials.service.ts @@ -0,0 +1,43 @@ +import { computed, Injectable, signal } from '@angular/core'; +import { Credentials } from '@core/entities'; + +const credentialsKey = 'credentials'; + +/** + * Provides storage for authentication credentials. + * The Credentials interface should be replaced with proper implementation. + */ +@Injectable({ + providedIn: 'root', +}) +export class CredentialsService { + /** The user credentials signal */ + readonly credentials = signal(this.loadCredentials()); + + /** Computed signal for checking authentication status */ + readonly isAuthenticated = computed(() => !!this.credentials()); + + private loadCredentials(): Credentials | null { + const savedCredentials = sessionStorage.getItem(credentialsKey) || localStorage.getItem(credentialsKey); + return savedCredentials ? JSON.parse(savedCredentials) : null; + } + + /** + * Sets the user credentials. + * The credentials may be persisted across sessions by setting the `remember` parameter to true. + * Otherwise, the credentials are only persisted for the current session. + * @param credentials The user credentials. + * @param remember True to remember credentials across sessions. + */ + setCredentials(credentials?: Credentials, remember = true) { + this.credentials.set(credentials || null); + + if (credentials) { + const storage = remember ? localStorage : sessionStorage; + storage.setItem(credentialsKey, JSON.stringify(credentials)); + } else { + sessionStorage.removeItem(credentialsKey); + localStorage.removeItem(credentialsKey); + } + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/services/permissions.service.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/services/permissions.service.ts new file mode 100644 index 000000000..cc085bb3a --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/auth/services/permissions.service.ts @@ -0,0 +1,80 @@ +import { computed, inject, Injectable } from '@angular/core'; +import { CredentialsService, PERMISSIONS, ROLE } from '@app/auth'; +import { appSetting } from '@core/constants'; + +@Injectable({ + providedIn: 'root', +}) +export class PermissionService { + private readonly credentialsService = inject(CredentialsService); + + /** Computed signal for the user's primary role */ + readonly userRole = computed(() => { + const credentials = this.credentialsService.credentials(); + return credentials?.roles[0] as ROLE; + }); + + /** + * The function `hasRole` checks if a user has any of the required roles based on their credentials. + * @param {ROLE[]} requiredRoles - The `requiredRoles` parameter in the `hasRole` method is an array of + * `ROLE` values that represent the roles that a user must have in order to pass the authorization + * check. The method checks if the user's credentials contain any of the roles specified in the + * `requiredRoles` array. + * @returns The `hasRole` method is returning a boolean value. It checks if the user's credentials + * contain any of the required roles specified in the `requiredRoles` array. If at least one of the + * required roles matches with the user's roles, it returns `true`. Otherwise, it returns `false`. + */ + hasRole(requiredRoles: ROLE[]): boolean { + const credentials = this.credentialsService.credentials(); + if (!credentials || !credentials.roles) { + return false; + } + // Check if any of the user's roles match the required roles + return requiredRoles.some((role) => credentials.roles.includes(role)); + } + + /** + * The function `hasPermission` checks if a user has the required permission based on their roles and + * application settings. + * @param {PERMISSIONS} permission - The `permission` parameter represents the permission that needs to + * be checked. This permission is typically a value from an enum or a set of predefined permissions in + * your application. + * @returns The `hasPermission` method returns a boolean value, either `true` or `false`, based on + * whether the user has the specified permission or not. + */ + hasPermission(permission: PERMISSIONS): boolean { + const credentials = this.credentialsService.credentials(); + if (!credentials) { + return false; + } + + const { roles } = credentials; + + for (const role of roles) { + // TODO: Implement the permissions as per your application requirements, provided above this file. + const rolePermissions = appSetting.role[role as ROLE]; + if (rolePermissions && this._checkPermission(rolePermissions, permission)) { + return true; + } + } + + return false; + } + + /* The `private _checkPermission` method is a helper function within the `PermissionService` class. + Its purpose is to check if a user has a specific permission based on their roles and the + application settings defined in the `appSetting` object. */ + private _checkPermission(rolePermissions: any, permission: PERMISSIONS): boolean { + const keys = permission.split('.'); + let currentLevel = rolePermissions; + + for (const key of keys) { + if (!currentLevel[key]) { + return false; + } + currentLevel = currentLevel[key]; + } + + return currentLevel === true; + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/i18n/i18n.service.spec.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/i18n/i18n.service.spec.ts new file mode 100644 index 000000000..5dcb5c9fb --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/i18n/i18n.service.spec.ts @@ -0,0 +1,132 @@ +import { TestBed } from '@angular/core/testing'; +import { TranslateService, LangChangeEvent } from '@ngx-translate/core'; +import { Subject } from 'rxjs'; + +import { I18nService } from './i18n.service'; + +const defaultLanguage = 'en-US'; +const supportedLanguages = ['eo', 'en-US', 'fr-FR']; + +class MockTranslateService { + currentLang = ''; + onLangChange = new Subject(); + + use(language: string) { + this.currentLang = language; + this.onLangChange.next({ + lang: this.currentLang, + translations: {}, + }); + } + + getBrowserCultureLang() { + return 'en-US'; + } + + setTranslation(lang: string, translations: object, shouldMerge?: boolean) {} +} + +describe('I18nService', () => { + let i18nService: I18nService; + let translateService: TranslateService; + let onLangChangeSpy: jasmine.Spy; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [I18nService, { provide: TranslateService, useClass: MockTranslateService }], + }); + + i18nService = TestBed.inject(I18nService); + translateService = TestBed.inject(TranslateService); + + // Create spies + onLangChangeSpy = jasmine.createSpy('onLangChangeSpy'); + translateService.onLangChange.subscribe((event: LangChangeEvent) => { + onLangChangeSpy(event.lang); + }); + spyOn(translateService, 'use').and.callThrough(); + }); + + afterEach(() => { + // Cleanup + localStorage.removeItem('language'); + }); + + describe('init', () => { + it('should init with default language', () => { + // Act + i18nService.init(defaultLanguage, supportedLanguages); + + // Assert + expect(translateService.use).toHaveBeenCalledWith(defaultLanguage); + expect(onLangChangeSpy).toHaveBeenCalledWith(defaultLanguage); + }); + + it('should init with save language', () => { + // Arrange + const savedLanguage = 'eo'; + localStorage.setItem('language', savedLanguage); + + // Act + i18nService.init(defaultLanguage, supportedLanguages); + + // Assert + expect(translateService.use).toHaveBeenCalledWith(savedLanguage); + expect(onLangChangeSpy).toHaveBeenCalledWith(savedLanguage); + }); + }); + + describe('set language', () => { + it('should change current language', () => { + // Arrange + const newLanguage = 'eo'; + i18nService.init(defaultLanguage, supportedLanguages); + + // Act + i18nService.language = newLanguage; + + // Assert + expect(translateService.use).toHaveBeenCalledWith(newLanguage); + expect(onLangChangeSpy).toHaveBeenCalledWith(newLanguage); + }); + + it('should change current language without a region match', () => { + // Arrange + const newLanguage = 'fr-CA'; + i18nService.init(defaultLanguage, supportedLanguages); + + // Act + i18nService.language = newLanguage; + + // Assert + expect(translateService.use).toHaveBeenCalledWith('fr-FR'); + expect(onLangChangeSpy).toHaveBeenCalledWith('fr-FR'); + }); + + it('should change current language to default if unsupported', () => { + // Arrange + const newLanguage = 'es'; + i18nService.init(defaultLanguage, supportedLanguages); + + // Act + i18nService.language = newLanguage; + + // Assert + expect(translateService.use).toHaveBeenCalledWith(defaultLanguage); + expect(onLangChangeSpy).toHaveBeenCalledWith(defaultLanguage); + }); + }); + + describe('get language', () => { + it('should return current language', () => { + // Arrange + i18nService.init(defaultLanguage, supportedLanguages); + + // Act + const currentLanguage = i18nService.language; + + // Assert + expect(currentLanguage).toEqual(defaultLanguage); + }); + }); +}); diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/i18n/i18n.service.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/i18n/i18n.service.ts new file mode 100644 index 000000000..2f0fb7237 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/i18n/i18n.service.ts @@ -0,0 +1,105 @@ +import { DestroyRef, effect, inject, Injectable, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { LangChangeEvent, TranslateService } from '@ngx-translate/core'; + +import enUS from '../../translations/en-US.json'; +import { Logger } from '@app/@core/services'; +import { environment } from '@env/environment'; + +const log = new Logger('I18nService'); +const languageKey = 'language'; + +@Injectable({ + providedIn: 'root', +}) +export class I18nService { + private readonly translateService = inject(TranslateService); + private readonly destroyRef = inject(DestroyRef); + + /** Current language signal */ + readonly currentLanguage = signal(localStorage.getItem(languageKey) || environment.defaultLanguage || this.translateService.getBrowserCultureLang() || ''); + + /** Default language */ + readonly defaultLanguage = signal(''); + + /** Supported languages */ + readonly supportedLanguages = signal([]); + + constructor() { + // Embed English translation + this.translateService.setTranslation('en-US', enUS); + + // Effect to persist language changes to localStorage + effect(() => { + const lang = this.currentLanguage(); + if (lang) { + localStorage.setItem(languageKey, lang); + } + }); + } + + /** + * Gets the current language from TranslateService. + * @return The current language code. + */ + get language(): string { + return this.translateService.currentLang; + } + + /** + * Sets the current language. + * Note: The current language is saved to the local storage. + * If no parameter is specified, the language is loaded from local storage (if present). + * @param language The IETF language code to set. + */ + setLanguage(language: string): void { + let newLanguage = language || localStorage.getItem(languageKey) || environment.defaultLanguage || this.translateService.getBrowserCultureLang() || ''; + let isSupportedLanguage = this.supportedLanguages().includes(newLanguage); + + // If no exact match is found, search without the region + if (newLanguage && !isSupportedLanguage) { + const langPrefix = newLanguage.split('-')[0]; + newLanguage = this.supportedLanguages().find((supportedLanguage) => supportedLanguage.startsWith(langPrefix)) || ''; + isSupportedLanguage = Boolean(newLanguage); + } + + // Fallback if language is not supported + if (!newLanguage || !isSupportedLanguage) { + newLanguage = this.defaultLanguage(); + } + + if (newLanguage !== this.currentLanguage()) { + this.currentLanguage.set(newLanguage); + } + + log.debug(`Language set to ${newLanguage}`); + this.translateService.use(newLanguage); + } + + /** + * Initializes i18n for the application. + * Loads language from local storage if present, or sets default language. + * @param defaultLanguage The default language to use. + * @param supportedLanguages The list of supported languages. + */ + init(defaultLanguage: string, supportedLanguages: string[]) { + this.defaultLanguage.set(defaultLanguage); + this.supportedLanguages.set(supportedLanguages); + log.debug(`Initializing with default language: ${defaultLanguage}`); + this.setLanguage(''); + + // Subscribe to language changes from TranslateService + this.translateService.onLangChange.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event: LangChangeEvent) => { + if (event.lang !== this.currentLanguage()) { + this.currentLanguage.set(event.lang); + } + }); + } + + /** + * @deprecated Use init() which handles cleanup automatically via DestroyRef + */ + destroy() { + // No longer needed - DestroyRef handles cleanup automatically + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/i18n/index.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/i18n/index.ts new file mode 100644 index 000000000..15dc79784 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/i18n/index.ts @@ -0,0 +1,2 @@ +export * from './i18n.service'; +export * from './language-selector.component'; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/i18n/language-selector.component.html b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/i18n/language-selector.component.html new file mode 100644 index 000000000..87ea217c0 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/i18n/language-selector.component.html @@ -0,0 +1,17 @@ +
+ + @if (isDropdownOpen()) { +
+
    + @for (language of languages(); track language) { +
  • +
    + {{ language }} +
  • + } @empty { +
  • There are no items.
  • + } +
+
+ } +
diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/i18n/language-selector.component.scss b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/i18n/language-selector.component.scss new file mode 100644 index 000000000..a1d304fa9 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/i18n/language-selector.component.scss @@ -0,0 +1,53 @@ +.language-selector { + max-width: max-content; +} + +.nav-item { + box-sizing: border-box; + display: inline-block; + width: 100%; + position: relative; + + .dropdown-toggle { + cursor: pointer; + user-select: none; + color: var(--slate-gray); + font-size: 14px; + font-style: normal; + font-weight: 600; + line-height: normal; + display: flex; + align-items: center; + gap: 7px; + } + + .caret { + border-top: 4px solid; + border-right: 4px solid transparent; + border-left: 4px solid transparent; + display: inline-block; + margin-left: 5px; + } + + .selectable-list-popover { + position: absolute; + right: 10px; + margin-top: 16px; + z-index: 100; + } +} + +// Show dropdown menu when `isDropdownOpen` is true +.nav-item.open .selectable-list-popover { + display: block; +} + +.above { + bottom: 100%; + top: auto; + margin-top: 0; + margin-bottom: 16px; + //transform: translateY(-8px); + position: absolute; + right: 0px; +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/i18n/language-selector.component.spec.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/i18n/language-selector.component.spec.ts new file mode 100644 index 000000000..333db58ff --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/i18n/language-selector.component.spec.ts @@ -0,0 +1,28 @@ +import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; + +import { I18nService } from './i18n.service'; +import { LanguageSelectorComponent } from './language-selector.component'; + +describe('LanguageSelectorComponent', () => { + let component: LanguageSelectorComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [LanguageSelectorComponent], + providers: [I18nService], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LanguageSelectorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/i18n/language-selector.component.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/i18n/language-selector.component.ts new file mode 100644 index 000000000..b351cb021 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/i18n/language-selector.component.ts @@ -0,0 +1,49 @@ +import { Component, ElementRef, input, inject, ChangeDetectionStrategy, signal, computed } from '@angular/core'; +import { I18nService } from './i18n.service'; + +@Component({ + selector: 'app-language-selector', + templateUrl: './language-selector.component.html', + styleUrls: ['./language-selector.component.scss'], + imports: [], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '(document:click)': 'onClickOutside($event)', + }, +}) +export class LanguageSelectorComponent { + private readonly i18nService = inject(I18nService); + private readonly eRef = inject(ElementRef); + + inNavbar = input(true); + openAbove = input(false); + isDropdownOpen = signal(false); + + readonly currentLanguageCode = computed(() => { + const language = this.i18nService.currentLanguage(); + const parts = language.split('-'); + return parts.length > 1 ? parts[1] : ''; + }); + + readonly languages = computed(() => this.i18nService.supportedLanguages()); + + onClickOutside(event: Event) { + if (!this.eRef?.nativeElement?.contains(event.target)) { + this.isDropdownOpen.set(false); + } + } + + toggleDropdown() { + this.isDropdownOpen.update((v) => !v); + } + + setLanguage(language: string) { + this.i18nService.setLanguage(language); + this.isDropdownOpen.set(false); + } + + getLanguageCode(language: string): string { + const parts = language.split('-'); + return parts.length > 1 ? parts[1] : ''; + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/billing/billing.component.html b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/billing/billing.component.html new file mode 100644 index 000000000..bdcf22a23 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/billing/billing.component.html @@ -0,0 +1,78 @@ +
+

Billing & Subscription

+ + +
+
+ Current Plan +

{{ currentPlanDetails()?.name }}

+

Your plan renews on March 1, 2024

+
+
+ ${{ currentPlanDetails()?.price }}/month + +
+
+ + +

Available Plans

+
+ @for (plan of plans; track plan.id) { +
+ @if (plan.recommended) { + Recommended + } +

{{ plan.name }}

+
+ ${{ plan.price }} + /{{ plan.period }} +
+
    + @for (feature of plan.features; track feature) { +
  • + + {{ feature }} +
  • + } +
+ +
+ } +
+ + +

Payment Method

+
+
+
💳
+
+

•••• •••• •••• 4242

+

Expires 12/25

+
+
+ +
+ + +

Invoice History

+
+
+ Invoice + Date + Amount + Status + +
+ @for (invoice of invoices; track invoice.id) { +
+ {{ invoice.id }} + {{ invoice.date }} + ${{ invoice.amount }}.00 + {{ invoice.status | titlecase }} + +
+ } +
+
diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/billing/billing.component.scss b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/billing/billing.component.scss new file mode 100644 index 000000000..c78d31867 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/billing/billing.component.scss @@ -0,0 +1,284 @@ +.billing-page { + h2 { + margin: 0 0 1.5rem 0; + font-size: 1.5rem; + font-weight: 600; + } + + .section-title { + margin: 2rem 0 1rem 0; + font-size: 1rem; + font-weight: 600; + } +} + +.current-plan-card { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.5rem; + background: linear-gradient(135deg, #f97316 0%, #ea580c 100%); + border-radius: 12px; + color: white; + + .plan-info { + .label { + font-size: 0.75rem; + opacity: 0.9; + text-transform: uppercase; + letter-spacing: 0.05em; + } + + h3 { + margin: 0.25rem 0; + font-size: 1.5rem; + font-weight: 700; + } + + p { + margin: 0; + font-size: 0.875rem; + opacity: 0.9; + } + } + + .plan-actions { + display: flex; + align-items: center; + gap: 1.5rem; + + .plan-price { + font-size: 1.5rem; + font-weight: 700; + + .period { + font-size: 0.875rem; + font-weight: 400; + opacity: 0.8; + } + } + } + + .btn-outline { + padding: 0.5rem 1rem; + background: rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.5); + color: white; + border-radius: 6px; + cursor: pointer; + font-weight: 500; + + &:hover { + background: rgba(255, 255, 255, 0.3); + } + } +} + +.plans-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1.5rem; +} + +.plan-card { + position: relative; + background: var(--white); + border: 1px solid #e5e7eb; + border-radius: 12px; + padding: 1.5rem; + transition: all 0.2s; + + &:hover { + border-color: #d1d5db; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + } + + &.recommended { + border-color: #f97316; + box-shadow: 0 4px 12px rgba(249, 115, 22, 0.2); + } + + &.current { + background: #f9fafb; + } + + .recommended-badge { + position: absolute; + top: -12px; + left: 50%; + transform: translateX(-50%); + padding: 0.25rem 0.75rem; + background: #f97316; + color: white; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; + } + + h4 { + margin: 0.5rem 0; + font-size: 1.125rem; + font-weight: 600; + } + + .price { + margin: 1rem 0; + + .amount { + font-size: 2rem; + font-weight: 700; + } + + .period { + font-size: 0.875rem; + color: var(--text-color-medium, #6b7280); + } + } + + .features { + list-style: none; + padding: 0; + margin: 0 0 1.5rem 0; + + li { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0; + font-size: 0.875rem; + + .check { + color: #10b981; + } + } + } + + button { + width: 100%; + padding: 0.75rem; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + + &.btn-primary { + background: linear-gradient(135deg, #f97316 0%, #ea580c 100%); + color: white; + border: none; + } + + &.btn-outline { + background: white; + border: 1px solid #e5e7eb; + color: var(--text-color-dark, #1f2937); + + &:hover:not(:disabled) { + background: #f9fafb; + } + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } + } +} + +.payment-card { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 1.5rem; + background: var(--white); + border: 1px solid #e5e7eb; + border-radius: 8px; + + .card-info { + display: flex; + align-items: center; + gap: 1rem; + + .card-icon { + font-size: 1.5rem; + } + + .card-number { + margin: 0; + font-weight: 500; + } + + .card-expiry { + margin: 0.25rem 0 0 0; + font-size: 0.75rem; + color: var(--text-color-medium, #6b7280); + } + } +} + +.btn-text { + background: none; + border: none; + color: #f97316; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + + &:hover { + text-decoration: underline; + } +} + +.invoices-table { + background: var(--white); + border: 1px solid #e5e7eb; + border-radius: 8px; + overflow: hidden; + + .table-header { + display: grid; + grid-template-columns: 1fr 1fr 1fr 100px 80px; + gap: 1rem; + padding: 0.75rem 1.5rem; + background: #f9fafb; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + color: var(--text-color-medium, #6b7280); + } + + .table-row { + display: grid; + grid-template-columns: 1fr 1fr 1fr 100px 80px; + gap: 1rem; + padding: 1rem 1.5rem; + border-top: 1px solid #e5e7eb; + font-size: 0.875rem; + align-items: center; + + .invoice-id { + font-weight: 500; + } + + .status { + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 500; + text-align: center; + + &.paid { + background: #dcfce7; + color: #16a34a; + } + + &.pending { + background: #fef3c7; + color: #d97706; + } + + &.failed { + background: #fee2e2; + color: #dc2626; + } + } + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/billing/billing.component.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/billing/billing.component.ts new file mode 100644 index 000000000..ec7c6db49 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/billing/billing.component.ts @@ -0,0 +1,65 @@ +import { Component, signal, computed, ChangeDetectionStrategy } from '@angular/core'; +import { TitleCasePipe } from '@angular/common'; + +interface Plan { + id: string; + name: string; + price: number; + period: string; + features: string[]; + recommended: boolean; +} + +interface Invoice { + id: string; + date: string; + amount: number; + status: 'paid' | 'pending' | 'failed'; +} + +@Component({ + selector: 'app-billing', + imports: [TitleCasePipe], + templateUrl: './billing.component.html', + styleUrl: './billing.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BillingComponent { + readonly currentPlan = signal('starter'); + + readonly plans: Plan[] = [ + { + id: 'free', + name: 'Free', + price: 0, + period: 'month', + features: ['Up to 5 users', '1 project', 'Basic support', '1GB storage'], + recommended: false, + }, + { + id: 'starter', + name: 'Starter', + price: 29, + period: 'month', + features: ['Up to 25 users', '10 projects', 'Priority support', 'API access', '10GB storage'], + recommended: true, + }, + { + id: 'pro', + name: 'Pro', + price: 99, + period: 'month', + features: ['Unlimited users', 'Unlimited projects', '24/7 support', 'Advanced analytics', 'Custom integrations', '100GB storage'], + recommended: false, + }, + ]; + + readonly invoices: Invoice[] = [ + { id: 'INV-001', date: 'Feb 1, 2024', amount: 29, status: 'paid' }, + { id: 'INV-002', date: 'Jan 1, 2024', amount: 29, status: 'paid' }, + { id: 'INV-003', date: 'Dec 1, 2023', amount: 29, status: 'paid' }, + { id: 'INV-004', date: 'Nov 1, 2023', amount: 29, status: 'paid' }, + ]; + + readonly currentPlanDetails = computed(() => this.plans.find((p) => p.id === this.currentPlan())); +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/dashboard/components/activity-feed/activity-feed.component.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/dashboard/components/activity-feed/activity-feed.component.ts new file mode 100644 index 000000000..6e100ff6c --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/dashboard/components/activity-feed/activity-feed.component.ts @@ -0,0 +1,146 @@ +import { Component, inject, ChangeDetectionStrategy } from '@angular/core'; +import { DataService } from '@app/@core/services/data.service'; + +@Component({ + selector: 'app-activity-feed', + imports: [], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+
+

Recent Activity

+ {{ dataService.activities().length }} activities +
+
+ @if (dataService.activities().length === 0) { +
+ 📋 +

No activity yet. Create a project or add a member to get started!

+
+ } @else { + @for (activity of dataService.activities(); track activity.id) { +
+
{{ activity.avatar }}
+
+

+ {{ activity.user }} + {{ activity.action }} + {{ activity.target }} +

+ {{ getTimeAgo(activity.time) }} +
+
+ } + } +
+
+ `, + styles: [ + ` + .activity-feed { + background: var(--white); + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + } + + .feed-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + + h3 { + margin: 0; + font-size: 1rem; + font-weight: 600; + } + + .activity-count { + font-size: 0.75rem; + color: var(--text-color-medium, #6b7280); + background: #f3f4f6; + padding: 0.25rem 0.5rem; + border-radius: 4px; + } + } + + .feed-list { + display: flex; + flex-direction: column; + gap: 1rem; + max-height: 300px; + overflow-y: auto; + } + + .empty-state { + text-align: center; + padding: 2rem 1rem; + color: var(--text-color-medium, #6b7280); + + .empty-icon { + font-size: 2rem; + display: block; + margin-bottom: 0.5rem; + } + + p { + margin: 0; + font-size: 0.875rem; + } + } + + .activity-item { + display: flex; + gap: 0.75rem; + align-items: flex-start; + } + + .activity-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + background: #f3f4f6; + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; + flex-shrink: 0; + } + + .activity-content { + flex: 1; + + p { + margin: 0; + font-size: 0.875rem; + color: var(--text-color-dark, #1f2937); + line-height: 1.4; + + .target { + color: #f97316; + font-weight: 500; + } + } + + .activity-time { + font-size: 0.75rem; + color: var(--text-color-medium, #6b7280); + } + } + `, + ], +}) +export class ActivityFeedComponent { + readonly dataService = inject(DataService); + + getTimeAgo(date: Date): string { + const now = new Date(); + const seconds = Math.floor((now.getTime() - new Date(date).getTime()) / 1000); + + if (seconds < 60) return 'Just now'; + if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes ago`; + if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours ago`; + return `${Math.floor(seconds / 86400)} days ago`; + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/dashboard/components/quick-actions/quick-actions.component.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/dashboard/components/quick-actions/quick-actions.component.ts new file mode 100644 index 000000000..5f92b3e81 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/dashboard/components/quick-actions/quick-actions.component.ts @@ -0,0 +1,111 @@ +import { Component, output, inject, ChangeDetectionStrategy } from '@angular/core'; +import { Router } from '@angular/router'; +import { HotToastService } from '@ngxpert/hot-toast'; + +interface QuickAction { + id: string; + label: string; + icon: string; + color: string; +} + +@Component({ + selector: 'app-quick-actions', + imports: [], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+

Quick Actions

+
+ @for (action of actions; track action.id) { + + } +
+
+ `, + styles: [ + ` + .quick-actions { + background: var(--white); + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + + h3 { + margin: 0 0 1rem 0; + font-size: 1rem; + font-weight: 600; + } + } + + .actions-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.75rem; + } + + .action-btn { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 1rem; + border: 1px solid #e5e7eb; + border-radius: 8px; + background: white; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + border-color: var(--action-color, #f97316); + background: rgba(249, 115, 22, 0.05); + } + + .action-icon { + font-size: 1.25rem; + } + + .action-label { + font-size: 0.75rem; + font-weight: 500; + color: var(--text-color-dark, #1f2937); + } + } + `, + ], +}) +export class QuickActionsComponent { + private readonly router = inject(Router); + private readonly toast = inject(HotToastService); + + newProject = output(); + addMember = output(); + + actions: QuickAction[] = [ + { id: 'new-project', label: 'New Project', icon: '➕', color: '#10b981' }, + { id: 'add-member', label: 'Add Member', icon: '👤', color: '#3b82f6' }, + { id: 'upload-file', label: 'Upload File', icon: '📁', color: '#8b5cf6' }, + { id: 'view-reports', label: 'View Reports', icon: '📊', color: '#f97316' }, + ]; + + onAction(actionId: string) { + switch (actionId) { + case 'new-project': + this.newProject.emit(); + break; + case 'add-member': + this.addMember.emit(); + break; + case 'upload-file': + this.toast.info('File upload coming soon!'); + break; + case 'view-reports': + this.toast.info('Reports coming soon!'); + break; + } + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/dashboard/components/stats-card/stats-card.component.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/dashboard/components/stats-card/stats-card.component.ts new file mode 100644 index 000000000..535152c61 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/dashboard/components/stats-card/stats-card.component.ts @@ -0,0 +1,86 @@ +import { Component, input, ChangeDetectionStrategy } from '@angular/core'; + +@Component({ + selector: 'app-stats-card', + imports: [], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+
+ {{ icon() }} +
+
+ {{ label() }} +

{{ value() }}

+ @if (changePercent() !== undefined) { + {{ changePercent()! >= 0 ? '+' : '' }}{{ changePercent() }}% + } +
+
+ `, + styles: [ + ` + .stats-card { + background: var(--white); + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + display: flex; + align-items: center; + gap: 1rem; + transition: box-shadow 0.2s ease; + + &:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + } + } + + .stats-icon { + width: 48px; + height: 48px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.25rem; + } + + .stats-content { + flex: 1; + } + + .stats-label { + font-size: 0.875rem; + color: var(--text-color-medium, #6b7280); + display: block; + } + + .stats-value { + font-size: 1.75rem; + font-weight: 700; + margin: 0.25rem 0; + color: var(--text-color-dark, #1f2937); + } + + .stats-change { + font-size: 0.75rem; + font-weight: 600; + + &.positive { + color: #10b981; + } + + &.negative { + color: #ef4444; + } + } + `, + ], +}) +export class StatsCardComponent { + label = input(''); + value = input(0); + icon = input('📊'); + iconBg = input('rgba(249, 115, 22, 0.1)'); + changePercent = input(undefined); +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/dashboard/components/welcome-banner/welcome-banner.component.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/dashboard/components/welcome-banner/welcome-banner.component.ts new file mode 100644 index 000000000..7e9d36c73 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/dashboard/components/welcome-banner/welcome-banner.component.ts @@ -0,0 +1,54 @@ +import { Component, input, ChangeDetectionStrategy } from '@angular/core'; + +@Component({ + selector: 'app-welcome-banner', + imports: [], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+
+

Welcome back, {{ userName() || 'there' }}!

+

Here's what's happening with your {{ organizationName() || 'workspace' }} today.

+
+
+ 🚀 +
+
+ `, + styles: [ + ` + .welcome-banner { + background: linear-gradient(135deg, #f97316 0%, #ea580c 100%); + border-radius: 16px; + padding: 2rem; + color: white; + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; + } + + .welcome-content { + h1 { + font-size: 1.5rem; + font-weight: 700; + margin: 0 0 0.5rem 0; + } + + p { + margin: 0; + opacity: 0.9; + font-size: 0.95rem; + } + } + + .welcome-illustration { + font-size: 3rem; + } + `, + ], +}) +export class WelcomeBannerComponent { + userName = input(undefined); + organizationName = input(undefined); +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/dashboard/dashboard.component.html b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/dashboard/dashboard.component.html new file mode 100644 index 000000000..6cb84fa05 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/dashboard/dashboard.component.html @@ -0,0 +1,26 @@ +
+ + + + +
+ @for (stat of stats(); track stat.label) { + + } +
+ + +
+
+ +
+
+ +
+
+
+ + + + + diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/dashboard/dashboard.component.scss b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/dashboard/dashboard.component.scss new file mode 100644 index 000000000..ffc4f331b --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/dashboard/dashboard.component.scss @@ -0,0 +1,26 @@ +.dashboard { + .stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; + } + + .dashboard-grid { + display: grid; + grid-template-columns: 1fr 320px; + gap: 1.5rem; + + @media (max-width: 1024px) { + grid-template-columns: 1fr; + } + } + + .main-column { + min-width: 0; + } + + .side-column { + min-width: 0; + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/dashboard/dashboard.component.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/dashboard/dashboard.component.ts new file mode 100644 index 000000000..6ae0acead --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/dashboard/dashboard.component.ts @@ -0,0 +1,55 @@ +import { Component, inject, signal, computed, ChangeDetectionStrategy } from '@angular/core'; +import { CredentialsService } from '@app/auth/services/credentials.service'; +import { DataService } from '@app/@core/services/data.service'; +import { StatsCardComponent } from './components/stats-card/stats-card.component'; +import { WelcomeBannerComponent } from './components/welcome-banner/welcome-banner.component'; +import { ActivityFeedComponent } from './components/activity-feed/activity-feed.component'; +import { QuickActionsComponent } from './components/quick-actions/quick-actions.component'; +import { CreateProjectModalComponent } from '@app/shared/components/create-project-modal/create-project-modal.component'; +import { AddMemberModalComponent } from '@app/shared/components/add-member-modal/add-member-modal.component'; + +interface StatItem { + label: string; + value: string | number; + icon: string; + iconBg: string; + change?: number; +} + +@Component({ + selector: 'app-dashboard', + imports: [StatsCardComponent, WelcomeBannerComponent, ActivityFeedComponent, QuickActionsComponent, CreateProjectModalComponent, AddMemberModalComponent], + templateUrl: './dashboard.component.html', + styleUrl: './dashboard.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DashboardComponent { + readonly credentialsService = inject(CredentialsService); + private readonly dataService = inject(DataService); + + readonly showCreateProjectModal = signal(false); + readonly showAddMemberModal = signal(false); + + readonly stats = computed(() => { + const dataStats = this.dataService.stats(); + return [ + { label: 'Team Members', value: dataStats.totalMembers, icon: '👥', iconBg: 'rgba(59, 130, 246, 0.1)' }, + { label: 'Active Projects', value: dataStats.activeProjects, icon: '📁', iconBg: 'rgba(139, 92, 246, 0.1)' }, + { label: 'Total Projects', value: dataStats.totalProjects, icon: '📊', iconBg: 'rgba(16, 185, 129, 0.1)' }, + { label: 'Activities', value: dataStats.recentActivities, icon: '📋', iconBg: 'rgba(249, 115, 22, 0.1)' }, + ]; + }); + + openCreateProjectModal() { + this.showCreateProjectModal.set(true); + } + + openAddMemberModal() { + this.showAddMemberModal.set(true); + } + + onModalClosed() { + this.showCreateProjectModal.set(false); + this.showAddMemberModal.set(false); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/pages-routing.module.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/pages-routing.module.ts new file mode 100644 index 000000000..e0674224f --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/pages-routing.module.ts @@ -0,0 +1,49 @@ +import { Routes } from '@angular/router'; +import { Shell } from '@app/shell/services/shell.service'; + +export const PAGES_ROUTES: Routes = [ + Shell.childRoutes([ + { + path: 'dashboard', + loadComponent: () => import('@pages/dashboard/dashboard.component').then((m) => m.DashboardComponent), + }, + { + path: 'profile', + loadComponent: () => import('@pages/profile/profile.component').then((m) => m.ProfileComponent), + }, + { + path: 'settings', + loadComponent: () => import('@pages/settings/settings.component').then((m) => m.SettingsComponent), + children: [ + { path: '', redirectTo: 'account', pathMatch: 'full' }, + { + path: 'account', + loadComponent: () => import('@pages/settings/components/account-settings/account-settings.component').then((m) => m.AccountSettingsComponent), + }, + { + path: 'preferences', + loadComponent: () => import('@pages/settings/components/preferences-settings/preferences-settings.component').then((m) => m.PreferencesSettingsComponent), + }, + { + path: 'notifications', + loadComponent: () => import('@pages/settings/components/notification-settings/notification-settings.component').then((m) => m.NotificationSettingsComponent), + }, + { + path: 'security', + loadComponent: () => import('@pages/settings/components/security-settings/security-settings.component').then((m) => m.SecuritySettingsComponent), + }, + ], + }, + { + path: 'billing', + loadComponent: () => import('@pages/billing/billing.component').then((m) => m.BillingComponent), + }, + { + path: 'users', + loadChildren: () => import('./users/users.routes').then((m) => m.USERS_ROUTES), + }, + + // Fallback when no prior route is matched + { path: '**', redirectTo: '', pathMatch: 'full' }, + ]), +]; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/profile/profile.component.html b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/profile/profile.component.html new file mode 100644 index 000000000..cdc7b4958 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/profile/profile.component.html @@ -0,0 +1,78 @@ +
+ +
+
+
+ {{ userInitials() }} +
+ +
+ +
+ + +
+
+

Personal Information

+ +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + @if (isEditing()) { +
+ +
+ } +
+
+ + +
+
+

Account Information

+
+
+
+ Account ID + {{ credentialsService.credentials()?.id || 'N/A' }} +
+
+ Role + {{ credentialsService.credentials()?.roles?.[0] || 'User' }} +
+
+ Member Since + January 2024 +
+
+ Status + Active +
+
+
+
diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/profile/profile.component.scss b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/profile/profile.component.scss new file mode 100644 index 000000000..3f57ef43a --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/profile/profile.component.scss @@ -0,0 +1,203 @@ +.profile-page { + max-width: 800px; +} + +.profile-header { + display: flex; + align-items: center; + gap: 1.5rem; + margin-bottom: 2rem; + padding: 2rem; + background: var(--white); + border-radius: 12px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.avatar-section { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; +} + +.avatar { + width: 96px; + height: 96px; + border-radius: 50%; + background: linear-gradient(135deg, #f97316 0%, #ea580c 100%); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 2rem; + font-weight: 600; +} + +.btn-upload { + font-size: 0.75rem; + color: #f97316; + background: none; + border: none; + cursor: pointer; + + &:hover { + text-decoration: underline; + } +} + +.user-info { + h2 { + margin: 0 0 0.5rem 0; + font-size: 1.5rem; + font-weight: 600; + } + + .user-email { + margin: 0.5rem 0 0 0; + color: var(--text-color-medium, #6b7280); + font-size: 0.875rem; + } +} + +.role-badge { + display: inline-block; + padding: 0.25rem 0.75rem; + background: rgba(249, 115, 22, 0.1); + color: #ea580c; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; + text-transform: capitalize; +} + +.profile-card { + background: var(--white); + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + margin-bottom: 1.5rem; +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + padding-bottom: 1rem; + border-bottom: 1px solid #e5e7eb; + + h3 { + margin: 0; + font-size: 1rem; + font-weight: 600; + } +} + +.btn-edit { + padding: 0.5rem 1rem; + background: transparent; + border: 1px solid #e5e7eb; + border-radius: 6px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + + &:hover { + background: #f9fafb; + } +} + +.form-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + + @media (max-width: 640px) { + grid-template-columns: 1fr; + } +} + +.form-group { + display: flex; + flex-direction: column; + gap: 0.5rem; + + label { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-color-dark, #1f2937); + } + + input { + padding: 0.75rem 1rem; + border: 1px solid #e5e7eb; + border-radius: 8px; + font-size: 0.875rem; + transition: border-color 0.2s; + + &:focus { + outline: none; + border-color: #f97316; + } + + &.readonly { + background: #f9fafb; + color: var(--text-color-medium, #6b7280); + cursor: default; + } + } +} + +.form-actions { + margin-top: 1.5rem; + padding-top: 1rem; + border-top: 1px solid #e5e7eb; +} + +.btn-primary { + padding: 0.75rem 1.5rem; + background: linear-gradient(135deg, #f97316 0%, #ea580c 100%); + color: white; + border: none; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + transition: opacity 0.2s; + + &:hover { + opacity: 0.9; + } +} + +.info-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + + @media (max-width: 640px) { + grid-template-columns: 1fr; + } +} + +.info-item { + display: flex; + flex-direction: column; + gap: 0.25rem; + + .info-label { + font-size: 0.75rem; + color: var(--text-color-medium, #6b7280); + text-transform: uppercase; + letter-spacing: 0.05em; + } + + .info-value { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-color-dark, #1f2937); + + &.status-active { + color: #10b981; + } + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/profile/profile.component.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/profile/profile.component.ts new file mode 100644 index 000000000..3e84d2fab --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/profile/profile.component.ts @@ -0,0 +1,70 @@ +import { Component, OnInit, inject, signal, computed, ChangeDetectionStrategy } from '@angular/core'; +import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { CredentialsService } from '@app/auth/services/credentials.service'; +import { HotToastService } from '@ngxpert/hot-toast'; + +@Component({ + selector: 'app-profile', + imports: [ReactiveFormsModule], + templateUrl: './profile.component.html', + styleUrl: './profile.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ProfileComponent implements OnInit { + private readonly fb = inject(FormBuilder); + private readonly toast = inject(HotToastService); + readonly credentialsService = inject(CredentialsService); + + readonly isEditing = signal(false); + + readonly userInitials = computed(() => { + const user = this.credentialsService.credentials(); + if (!user) return '?'; + const first = user.firstName?.charAt(0) || ''; + const last = user.lastName?.charAt(0) || ''; + return (first + last).toUpperCase() || '?'; + }); + + profileForm: FormGroup = this.fb.group({ + firstName: ['', Validators.required], + lastName: ['', Validators.required], + email: ['', [Validators.required, Validators.email]], + username: ['', Validators.required], + }); + + ngOnInit() { + const user = this.credentialsService.credentials(); + if (user) { + this.profileForm.patchValue({ + firstName: user.firstName || '', + lastName: user.lastName || '', + email: user.email || '', + username: user.username || '', + }); + } + } + + toggleEdit() { + this.isEditing.update((v) => !v); + if (!this.isEditing()) { + const user = this.credentialsService.credentials(); + if (user) { + this.profileForm.patchValue({ + firstName: user.firstName || '', + lastName: user.lastName || '', + email: user.email || '', + username: user.username || '', + }); + } + } + } + + onSave() { + if (this.profileForm.valid) { + this.toast.success('Profile updated successfully'); + this.isEditing.set(false); + } else { + this.toast.error('Please fill in all required fields'); + } + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/settings/components/account-settings/account-settings.component.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/settings/components/account-settings/account-settings.component.ts new file mode 100644 index 000000000..04c6de9ee --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/settings/components/account-settings/account-settings.component.ts @@ -0,0 +1,218 @@ +import { Component, OnInit, inject, ChangeDetectionStrategy } from '@angular/core'; +import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { CredentialsService } from '@app/auth/services/credentials.service'; +import { HotToastService } from '@ngxpert/hot-toast'; + +@Component({ + selector: 'app-account-settings', + imports: [ReactiveFormsModule], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + `, + styles: [ + ` + .account-settings { + h3 { + margin: 0 0 0.5rem 0; + font-size: 1.125rem; + font-weight: 600; + } + + .description { + margin: 0 0 2rem 0; + color: var(--text-color-medium, #6b7280); + font-size: 0.875rem; + } + } + + .section { + margin-bottom: 2rem; + padding-bottom: 2rem; + border-bottom: 1px solid #e5e7eb; + + h4 { + margin: 0 0 1rem 0; + font-size: 0.875rem; + font-weight: 600; + color: var(--text-color-dark, #1f2937); + } + } + + .form-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + + .full-width { + grid-column: span 2; + } + + @media (max-width: 640px) { + grid-template-columns: 1fr; + + .full-width { + grid-column: span 1; + } + } + } + + .form-group { + display: flex; + flex-direction: column; + gap: 0.5rem; + + label { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-color-dark, #1f2937); + } + + input { + padding: 0.75rem 1rem; + border: 1px solid #e5e7eb; + border-radius: 8px; + font-size: 0.875rem; + + &:focus { + outline: none; + border-color: #f97316; + } + } + } + + .form-actions { + margin-bottom: 2rem; + } + + .btn-primary { + padding: 0.75rem 1.5rem; + background: linear-gradient(135deg, #f97316 0%, #ea580c 100%); + color: white; + border: none; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + + &:hover { + opacity: 0.9; + } + } + + .danger-zone { + padding: 1.5rem; + border: 1px solid #fecaca; + border-radius: 8px; + background: #fef2f2; + + h4 { + margin: 0 0 0.5rem 0; + font-size: 0.875rem; + font-weight: 600; + color: #dc2626; + } + + p { + margin: 0 0 1rem 0; + font-size: 0.875rem; + color: var(--text-color-dark, #1f2937); + } + + .btn-danger { + padding: 0.5rem 1rem; + background: #dc2626; + color: white; + border: none; + border-radius: 6px; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + + &:hover { + background: #b91c1c; + } + } + } + `, + ], +}) +export class AccountSettingsComponent implements OnInit { + private readonly fb = inject(FormBuilder); + private readonly credentialsService = inject(CredentialsService); + private readonly toast = inject(HotToastService); + + accountForm: FormGroup = this.fb.group({ + firstName: [''], + lastName: [''], + email: ['', Validators.email], + currentPassword: [''], + newPassword: [''], + confirmPassword: [''], + }); + + ngOnInit() { + const user = this.credentialsService.credentials(); + if (user) { + this.accountForm.patchValue({ + firstName: user.firstName || '', + lastName: user.lastName || '', + email: user.email || '', + }); + } + } + + onSave() { + this.toast.success('Account settings saved'); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/settings/components/notification-settings/notification-settings.component.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/settings/components/notification-settings/notification-settings.component.ts new file mode 100644 index 000000000..e5c70e2fa --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/settings/components/notification-settings/notification-settings.component.ts @@ -0,0 +1,230 @@ +import { Component, inject, ChangeDetectionStrategy, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { HotToastService } from '@ngxpert/hot-toast'; + +interface NotificationSettings { + productUpdates: boolean; + weeklyDigest: boolean; + teamActivity: boolean; + desktop: boolean; + sound: boolean; +} + +@Component({ + selector: 'app-notification-settings', + imports: [FormsModule], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+

Notifications

+

Manage how you receive notifications.

+ +
+

Email Notifications

+
+
+
+
Product Updates
+

News about product and feature updates

+
+ +
+ +
+
+
Weekly Digest
+

Weekly summary of your activity

+
+ +
+ +
+
+
Team Activity
+

Notifications when team members take actions

+
+ +
+
+
+ +
+

In-App Notifications

+
+
+
+
Desktop Notifications
+

Show notifications on your desktop

+
+ +
+ +
+
+
Sound
+

Play a sound for new notifications

+
+ +
+
+
+ +
+ +
+
+ `, + styles: [ + ` + .notification-settings { + h3 { + margin: 0 0 0.5rem 0; + font-size: 1.125rem; + font-weight: 600; + } + + .description { + margin: 0 0 2rem 0; + color: var(--text-color-medium, #6b7280); + font-size: 0.875rem; + } + } + + .section { + margin-bottom: 2rem; + + h4 { + margin: 0 0 1rem 0; + font-size: 0.875rem; + font-weight: 600; + color: var(--text-color-dark, #1f2937); + } + } + + .settings-list { + display: flex; + flex-direction: column; + } + + .setting-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 0; + border-bottom: 1px solid #e5e7eb; + + .setting-info { + h5 { + margin: 0 0 0.25rem 0; + font-size: 0.875rem; + font-weight: 500; + } + + p { + margin: 0; + font-size: 0.75rem; + color: var(--text-color-medium, #6b7280); + } + } + } + + .toggle { + position: relative; + display: inline-block; + width: 48px; + height: 24px; + + input { + opacity: 0; + width: 0; + height: 0; + } + + .slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #e5e7eb; + transition: 0.3s; + border-radius: 24px; + + &:before { + position: absolute; + content: ''; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + transition: 0.3s; + border-radius: 50%; + } + } + + input:checked + .slider { + background-color: #f97316; + } + + input:checked + .slider:before { + transform: translateX(24px); + } + } + + .form-actions { + padding-top: 1rem; + } + + .btn-primary { + padding: 0.75rem 1.5rem; + background: linear-gradient(135deg, #f97316 0%, #ea580c 100%); + color: white; + border: none; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + + &:hover { + opacity: 0.9; + } + } + `, + ], +}) +export class NotificationSettingsComponent { + private readonly toast = inject(HotToastService); + + readonly notifications = signal({ + productUpdates: true, + weeklyDigest: true, + teamActivity: false, + desktop: true, + sound: false, + }); + + updateSetting(key: keyof NotificationSettings, event: Event) { + const checked = (event.target as HTMLInputElement).checked; + this.notifications.update((current) => ({ ...current, [key]: checked })); + } + + saveNotifications() { + this.toast.success('Notification preferences saved'); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/settings/components/preferences-settings/preferences-settings.component.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/settings/components/preferences-settings/preferences-settings.component.ts new file mode 100644 index 000000000..756f8ddb9 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/settings/components/preferences-settings/preferences-settings.component.ts @@ -0,0 +1,249 @@ +import { Component, inject, ChangeDetectionStrategy, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { ThemeService } from '@app/shell/services/theme.service'; +import { HotToastService } from '@ngxpert/hot-toast'; + +interface Preferences { + isDarkMode: boolean; + isCompactView: boolean; + dateFormat: string; + timezone: string; + landingPage: string; +} + +@Component({ + selector: 'app-preferences-settings', + imports: [FormsModule], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+

Preferences

+

Customize your experience and display settings.

+ +
+
+
+

Dark Mode

+

Switch between light and dark themes

+
+ +
+ +
+
+

Compact View

+

Use a more condensed layout

+
+ +
+ +
+
+

Date Format

+

Choose your preferred date format

+
+ +
+ +
+
+

Timezone

+

Set your local timezone

+
+ +
+ +
+
+

Default Landing Page

+

Choose which page to show after login

+
+ +
+
+ +
+ +
+
+ `, + styles: [ + ` + .preferences-settings { + h3 { + margin: 0 0 0.5rem 0; + font-size: 1.125rem; + font-weight: 600; + } + + .description { + margin: 0 0 2rem 0; + color: var(--text-color-medium, #6b7280); + font-size: 0.875rem; + } + } + + .settings-list { + display: flex; + flex-direction: column; + gap: 1.5rem; + margin-bottom: 2rem; + } + + .setting-item { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 1.5rem; + border-bottom: 1px solid #e5e7eb; + + .setting-info { + h4 { + margin: 0 0 0.25rem 0; + font-size: 0.875rem; + font-weight: 600; + } + + p { + margin: 0; + font-size: 0.75rem; + color: var(--text-color-medium, #6b7280); + } + } + } + + .toggle { + position: relative; + display: inline-block; + width: 48px; + height: 24px; + + input { + opacity: 0; + width: 0; + height: 0; + } + + .slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #e5e7eb; + transition: 0.3s; + border-radius: 24px; + + &:before { + position: absolute; + content: ''; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + transition: 0.3s; + border-radius: 50%; + } + } + + input:checked + .slider { + background-color: #f97316; + } + + input:checked + .slider:before { + transform: translateX(24px); + } + } + + .select-input { + padding: 0.5rem 1rem; + border: 1px solid #e5e7eb; + border-radius: 6px; + font-size: 0.875rem; + background: white; + min-width: 180px; + + &:focus { + outline: none; + border-color: #f97316; + } + } + + .form-actions { + padding-top: 1rem; + } + + .btn-primary { + padding: 0.75rem 1.5rem; + background: linear-gradient(135deg, #f97316 0%, #ea580c 100%); + color: white; + border: none; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + + &:hover { + opacity: 0.9; + } + } + `, + ], +}) +export class PreferencesSettingsComponent { + private readonly themeService = inject(ThemeService); + private readonly toast = inject(HotToastService); + + readonly preferences = signal({ + isDarkMode: false, + isCompactView: false, + dateFormat: 'MM/DD/YYYY', + timezone: 'America/New_York', + landingPage: 'dashboard', + }); + + updatePreference(key: 'isDarkMode' | 'isCompactView', event: Event) { + const checked = (event.target as HTMLInputElement).checked; + this.preferences.update((current) => ({ ...current, [key]: checked })); + } + + updateSelectPreference(key: 'dateFormat' | 'timezone' | 'landingPage', event: Event) { + const value = (event.target as HTMLSelectElement).value; + this.preferences.update((current) => ({ ...current, [key]: value })); + } + + toggleTheme(event: Event) { + const isDarkMode = (event.target as HTMLInputElement).checked; + this.preferences.update((current) => ({ ...current, isDarkMode })); + if (isDarkMode) { + this.themeService.setDarkTheme(); + } else { + this.themeService.setLightTheme(); + } + } + + savePreferences() { + this.toast.success('Preferences saved'); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/settings/components/security-settings/security-settings.component.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/settings/components/security-settings/security-settings.component.ts new file mode 100644 index 000000000..f2d13848b --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/settings/components/security-settings/security-settings.component.ts @@ -0,0 +1,315 @@ +import { Component, inject, ChangeDetectionStrategy, signal } from '@angular/core'; +import { HotToastService } from '@ngxpert/hot-toast'; + +interface Session { + id: number; + device: string; + location: string; + lastActive: string; + current: boolean; +} + +interface LoginEntry { + id: number; + success: boolean; + location: string; + time: string; +} + +@Component({ + selector: 'app-security-settings', + imports: [], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+

Security

+

Manage your account security and active sessions.

+ +
+

Two-Factor Authentication

+
+
+
+ {{ tfaEnabled() ? '✓' : '✕' }} + {{ tfaEnabled() ? 'Enabled' : 'Disabled' }} +
+

Add an extra layer of security to your account by requiring a verification code in addition to your password.

+
+ +
+
+ +
+

Active Sessions

+
+ @for (session of sessions(); track session.id) { +
+
💻
+
+
{{ session.device }}
+

{{ session.location }} · {{ session.lastActive }}

+
+ @if (session.current) { + Current + } @else { + + } +
+ } +
+ +
+ +
+

Login History

+
+ @for (entry of loginHistory(); track entry.id) { +
+
+ {{ entry.success ? '✓' : '✕' }} +
+
+

{{ entry.success ? 'Successful login' : 'Failed login attempt' }}

+

{{ entry.location }} · {{ entry.time }}

+
+
+ } +
+
+
+ `, + styles: [ + ` + .security-settings { + h3 { + margin: 0 0 0.5rem 0; + font-size: 1.125rem; + font-weight: 600; + } + + .description { + margin: 0 0 2rem 0; + color: var(--text-color-medium, #6b7280); + font-size: 0.875rem; + } + } + + .section { + margin-bottom: 2rem; + + h4 { + margin: 0 0 1rem 0; + font-size: 0.875rem; + font-weight: 600; + color: var(--text-color-dark, #1f2937); + } + } + + .tfa-card { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.5rem; + background: #f9fafb; + border-radius: 8px; + + .tfa-info { + flex: 1; + + p { + margin: 0.5rem 0 0 0; + font-size: 0.875rem; + color: var(--text-color-medium, #6b7280); + max-width: 400px; + } + } + + .tfa-status { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 600; + font-size: 0.875rem; + color: #dc2626; + + &.enabled { + color: #10b981; + } + + .status-icon { + width: 20px; + height: 20px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + } + } + } + + .btn-primary { + padding: 0.5rem 1rem; + background: linear-gradient(135deg, #f97316 0%, #ea580c 100%); + color: white; + border: none; + border-radius: 6px; + font-weight: 500; + cursor: pointer; + } + + .btn-outline { + padding: 0.5rem 1rem; + background: white; + color: var(--text-color-dark, #1f2937); + border: 1px solid #e5e7eb; + border-radius: 6px; + font-weight: 500; + cursor: pointer; + + &:hover { + background: #f9fafb; + } + } + + .btn-text { + background: none; + border: none; + color: #f97316; + font-size: 0.875rem; + cursor: pointer; + + &:hover { + text-decoration: underline; + } + + &.revoke-all { + margin-top: 1rem; + } + } + + .sessions-list { + display: flex; + flex-direction: column; + } + + .session-item { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem 0; + border-bottom: 1px solid #e5e7eb; + + .session-icon { + font-size: 1.25rem; + } + + .session-info { + flex: 1; + + h5 { + margin: 0; + font-size: 0.875rem; + font-weight: 500; + } + + p { + margin: 0.25rem 0 0 0; + font-size: 0.75rem; + color: var(--text-color-medium, #6b7280); + } + } + + .current-badge { + padding: 0.25rem 0.5rem; + background: #dcfce7; + color: #16a34a; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 500; + } + } + + .history-list { + display: flex; + flex-direction: column; + } + + .history-item { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.75rem 0; + border-bottom: 1px solid #e5e7eb; + + .history-status { + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + + &.success { + background: #dcfce7; + color: #16a34a; + } + + &.failed { + background: #fee2e2; + color: #dc2626; + } + } + + .history-info { + .history-action { + margin: 0; + font-size: 0.875rem; + } + + .history-meta { + margin: 0.25rem 0 0 0; + font-size: 0.75rem; + color: var(--text-color-medium, #6b7280); + } + } + } + `, + ], +}) +export class SecuritySettingsComponent { + private readonly toast = inject(HotToastService); + + readonly tfaEnabled = signal(false); + + readonly sessions = signal([ + { id: 1, device: 'MacBook Pro - Chrome', location: 'San Francisco, CA', lastActive: 'Active now', current: true }, + { id: 2, device: 'iPhone 15 - Safari', location: 'San Francisco, CA', lastActive: '2 hours ago', current: false }, + { id: 3, device: 'Windows PC - Firefox', location: 'New York, NY', lastActive: '3 days ago', current: false }, + ]); + + readonly loginHistory = signal([ + { id: 1, success: true, location: 'San Francisco, CA', time: 'Today at 10:23 AM' }, + { id: 2, success: true, location: 'San Francisco, CA', time: 'Yesterday at 3:45 PM' }, + { id: 3, success: false, location: 'Unknown', time: 'Jan 15 at 8:12 AM' }, + { id: 4, success: true, location: 'New York, NY', time: 'Jan 14 at 2:30 PM' }, + ]); + + toggleTfa() { + this.tfaEnabled.update((v) => !v); + this.toast.success(this.tfaEnabled() ? '2FA enabled' : '2FA disabled'); + } + + revokeSession(session: Session) { + this.sessions.update((current) => current.filter((s) => s.id !== session.id)); + this.toast.success('Session revoked'); + } + + revokeAllSessions() { + this.sessions.update((current) => current.filter((s) => s.current)); + this.toast.success('All other sessions revoked'); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/settings/settings.component.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/settings/settings.component.ts new file mode 100644 index 000000000..0f80839f0 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/settings/settings.component.ts @@ -0,0 +1,113 @@ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +interface SettingsTab { + label: string; + path: string; + icon: string; +} + +@Component({ + selector: 'app-settings', + imports: [RouterModule], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+

Settings

+ +
+ + +
+ +
+
+
+ `, + styles: [ + ` + .settings-page { + h2 { + margin: 0 0 1.5rem 0; + font-size: 1.5rem; + font-weight: 600; + } + } + + .settings-layout { + display: grid; + grid-template-columns: 240px 1fr; + gap: 2rem; + + @media (max-width: 768px) { + grid-template-columns: 1fr; + } + } + + .settings-nav { + display: flex; + flex-direction: column; + gap: 0.25rem; + + @media (max-width: 768px) { + flex-direction: row; + overflow-x: auto; + padding-bottom: 1rem; + } + } + + .nav-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + border-radius: 8px; + color: var(--text-color-dark, #1f2937); + text-decoration: none; + transition: all 0.2s; + + &:hover { + background: #f3f4f6; + } + + &.active { + background: rgba(249, 115, 22, 0.1); + color: #ea580c; + font-weight: 500; + } + + .nav-icon { + font-size: 1.25rem; + } + + .nav-label { + font-size: 0.875rem; + white-space: nowrap; + } + } + + .settings-content { + background: var(--white); + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + min-height: 400px; + } + `, + ], +}) +export class SettingsComponent { + tabs: SettingsTab[] = [ + { label: 'Account', path: 'account', icon: '👤' }, + { label: 'Preferences', path: 'preferences', icon: '⚙️' }, + { label: 'Notifications', path: 'notifications', icon: '🔔' }, + { label: 'Security', path: 'security', icon: '🔒' }, + ]; +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/users/list/list.component.html b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/users/list/list.component.html new file mode 100644 index 000000000..64532c6a7 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/users/list/list.component.html @@ -0,0 +1,36 @@ +
+

Users List

+ +
+ + + + + + + + + + + + @if (!isLoading()) { + @for (user of users(); track user.id) { + + + + + + + + } + } @else { + + + + } + +
AvatarFull NameEmailPhoneLocation
+ {{ user.name.first }}'s avatar + {{ user.name.title }} {{ user.name.first }} {{ user.name.last }}{{ user.email }}{{ user.phone }}{{ user.location.city }}, {{ user.location.country }}
Loading...
+
+
diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/users/list/list.component.scss b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/users/list/list.component.scss new file mode 100644 index 000000000..537a88d5c --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/users/list/list.component.scss @@ -0,0 +1,25 @@ +.user-table-container { + margin: 20px; +} + +.user-table { + width: 100%; + border-collapse: collapse; + + th, + td { + padding: 10px; + border: 1px solid #ccc; + text-align: left; + } + + th { + background-color: #f4f4f4; + } + + img { + border-radius: 50%; + width: 50px; + height: 50px; + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/users/list/list.component.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/users/list/list.component.ts new file mode 100644 index 000000000..8eb5445a3 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/users/list/list.component.ts @@ -0,0 +1,34 @@ +import { Component, inject, OnInit, ChangeDetectionStrategy, signal } from '@angular/core'; +import { UseRandomUser } from '@core/usecases'; +import { RandomUserEntity } from '@core/entities'; +import { HotToastService } from '@ngxpert/hot-toast'; + +@Component({ + selector: 'app-list', + templateUrl: './list.component.html', + styleUrl: './list.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ListComponent implements OnInit { + readonly users = signal([]); + readonly isLoading = signal(true); + + private readonly useRandomUser = new UseRandomUser(); + private readonly toast = inject(HotToastService); + + ngOnInit() { + this.useRandomUser.getAllUsers().subscribe({ + next: (users) => { + this.users.set(users); + this.isLoading.set(false); + }, + error: (error) => { + console.error(error); + }, + }); + } + + userClicked() { + this.toast.show('User clicked'); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/users/users.routes.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/users/users.routes.ts new file mode 100644 index 000000000..a2d344c4d --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/pages/users/users.routes.ts @@ -0,0 +1,13 @@ +import { Routes } from '@angular/router'; + +export const USERS_ROUTES: Routes = [ + { + path: '', + redirectTo: 'list', + pathMatch: 'full', + }, + { + path: 'list', + loadComponent: () => import('./list/list.component').then((m) => m.ListComponent), + }, +]; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/components/add-member-modal/add-member-modal.component.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/components/add-member-modal/add-member-modal.component.ts new file mode 100644 index 000000000..6183ed634 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/components/add-member-modal/add-member-modal.component.ts @@ -0,0 +1,220 @@ +import { Component, input, output, inject, ChangeDetectionStrategy, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { ModalComponent } from '../modal/modal.component'; +import { DataService } from '@app/@core/services/data.service'; +import { HotToastService } from '@ngxpert/hot-toast'; + +interface MemberForm { + name: string; + email: string; + role: 'viewer' | 'member' | 'admin'; + avatar: string; +} + +@Component({ + selector: 'app-add-member-modal', + imports: [FormsModule, ModalComponent], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ @for (emoji of avatarOptions; track emoji) { + + } +
+
+ +
+ + +
+
+
+ `, + styles: [ + ` + .form-group { + margin-bottom: 1rem; + + label { + display: block; + margin-bottom: 0.5rem; + font-size: 0.875rem; + font-weight: 500; + } + + input, + select { + width: 100%; + padding: 0.75rem 1rem; + border: 1px solid #e5e7eb; + border-radius: 8px; + font-size: 0.875rem; + + &:focus { + outline: none; + border-color: #f97316; + } + } + + select { + background: white; + cursor: pointer; + } + } + + .avatar-picker { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + } + + .avatar-option { + width: 44px; + height: 44px; + border: 2px solid #e5e7eb; + border-radius: 50%; + background: white; + font-size: 1.25rem; + cursor: pointer; + transition: all 0.2s; + + &:hover { + border-color: #d1d5db; + } + + &.selected { + border-color: #f97316; + background: rgba(249, 115, 22, 0.1); + } + } + + .form-actions { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + margin-top: 1.5rem; + padding-top: 1rem; + border-top: 1px solid #e5e7eb; + } + + .btn-primary { + padding: 0.75rem 1.5rem; + background: linear-gradient(135deg, #f97316 0%, #ea580c 100%); + color: white; + border: none; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + + &:hover:not(:disabled) { + opacity: 0.9; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + } + + .btn-outline { + padding: 0.75rem 1.5rem; + background: transparent; + border: 1px solid #e5e7eb; + border-radius: 8px; + font-weight: 500; + cursor: pointer; + + &:hover { + background: #f9fafb; + } + } + `, + ], +}) +export class AddMemberModalComponent { + private readonly dataService = inject(DataService); + private readonly toast = inject(HotToastService); + + isOpen = input(false); + close = output(); + created = output(); + + readonly avatarOptions = ['👤', '👩', '👨', '🧑', '👩‍💼', '👨‍💼', '👩‍💻', '👨‍💻']; + + readonly member = signal({ + name: '', + email: '', + role: 'member', + avatar: '👤', + }); + + updateMember(key: 'name' | 'email', event: Event) { + const value = (event.target as HTMLInputElement).value; + this.member.update((current) => ({ ...current, [key]: value })); + } + + updateMemberSelect(key: 'role', event: Event) { + const value = (event.target as HTMLSelectElement).value as MemberForm['role']; + this.member.update((current) => ({ ...current, [key]: value })); + } + + selectAvatar(emoji: string) { + this.member.update((current) => ({ ...current, avatar: emoji })); + } + + onSubmit() { + const current = this.member(); + if (!current.name || !current.email) return; + + this.dataService.addMember({ + name: current.name, + email: current.email, + role: current.role, + avatar: current.avatar, + }); + + this.toast.success(`${current.name} added to the team!`); + this.resetForm(); + this.created.emit(); + this.close.emit(); + } + + onClose() { + this.resetForm(); + this.close.emit(); + } + + private resetForm() { + this.member.set({ + name: '', + email: '', + role: 'member', + avatar: '👤', + }); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/components/create-project-modal/create-project-modal.component.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/components/create-project-modal/create-project-modal.component.ts new file mode 100644 index 000000000..d4df06b19 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/components/create-project-modal/create-project-modal.component.ts @@ -0,0 +1,152 @@ +import { Component, input, output, inject, ChangeDetectionStrategy } from '@angular/core'; +import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms'; +import { ModalComponent } from '../modal/modal.component'; +import { DataService } from '@app/@core/services/data.service'; +import { HotToastService } from '@ngxpert/hot-toast'; + +@Component({ + selector: 'app-create-project-modal', + imports: [ReactiveFormsModule, ModalComponent], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ `, + styles: [ + ` + .form-group { + margin-bottom: 1rem; + + label { + display: block; + margin-bottom: 0.5rem; + font-size: 0.875rem; + font-weight: 500; + } + + input, + textarea, + select { + width: 100%; + padding: 0.75rem 1rem; + border: 1px solid #e5e7eb; + border-radius: 8px; + font-size: 0.875rem; + + &:focus { + outline: none; + border-color: #f97316; + } + } + + textarea { + resize: vertical; + } + + select { + background: white; + cursor: pointer; + } + } + + .form-actions { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + margin-top: 1.5rem; + padding-top: 1rem; + border-top: 1px solid #e5e7eb; + } + + .btn-primary { + padding: 0.75rem 1.5rem; + background: linear-gradient(135deg, #f97316 0%, #ea580c 100%); + color: white; + border: none; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + + &:hover:not(:disabled) { + opacity: 0.9; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + } + + .btn-outline { + padding: 0.75rem 1.5rem; + background: transparent; + border: 1px solid #e5e7eb; + border-radius: 8px; + font-weight: 500; + cursor: pointer; + + &:hover { + background: #f9fafb; + } + } + `, + ], +}) +export class CreateProjectModalComponent { + private readonly dataService = inject(DataService); + private readonly toast = inject(HotToastService); + private readonly fb = inject(FormBuilder); + + isOpen = input(false); + close = output(); + created = output(); + + projectForm = this.fb.nonNullable.group({ + name: ['', Validators.required], + description: [''], + status: ['active' as 'active' | 'on-hold' | 'completed'], + }); + + onSubmit() { + if (this.projectForm.invalid) return; + + const { name, description, status } = this.projectForm.getRawValue(); + + this.dataService.addProject({ name, description, status }); + + this.toast.success(`Project "${name}" created!`); + this.projectForm.reset({ name: '', description: '', status: 'active' }); + this.created.emit(); + this.close.emit(); + } + + onClose() { + this.projectForm.reset({ name: '', description: '', status: 'active' }); + this.close.emit(); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/components/index.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/components/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/components/modal/modal.component.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/components/modal/modal.component.ts new file mode 100644 index 000000000..fb925036d --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/components/modal/modal.component.ts @@ -0,0 +1,108 @@ +import { Component, input, output, ChangeDetectionStrategy, computed } from '@angular/core'; + +@Component({ + selector: 'app-modal', + imports: [], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + @if (isOpen()) { + + } + `, + styles: [ + ` + .modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 1rem; + } + + .modal-content { + background: var(--white, white); + border-radius: 12px; + width: 100%; + max-height: 90vh; + overflow-y: auto; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2); + } + + .modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 1.5rem; + border-bottom: 1px solid #e5e7eb; + + h3 { + margin: 0; + font-size: 1.125rem; + font-weight: 600; + } + + .close-btn { + width: 32px; + height: 32px; + border: none; + background: transparent; + font-size: 1.5rem; + cursor: pointer; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + color: #6b7280; + + &:hover { + background: #f3f4f6; + } + + &:focus { + outline: 2px solid #f97316; + outline-offset: 2px; + } + } + } + + .modal-body { + padding: 1.5rem; + } + `, + ], +}) +export class ModalComponent { + private static instanceCounter = 0; + private readonly instanceId = ++ModalComponent.instanceCounter; + + isOpen = input(false); + title = input(''); + maxWidth = input('480px'); + close = output(); + + readonly titleId = computed(() => `modal-title-${this.instanceId}`); + + onOverlayClick(event: MouseEvent) { + if ((event.target as HTMLElement).classList.contains('modal-overlay')) { + this.close.emit(); + } + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/directives/index.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/directives/index.ts new file mode 100644 index 000000000..920a4fd2e --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/directives/index.ts @@ -0,0 +1,2 @@ +export * from './letterWordCount.directive'; +export * from './number-input.directive'; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/directives/letterWordCount.directive.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/directives/letterWordCount.directive.ts new file mode 100644 index 000000000..f353d6a71 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/directives/letterWordCount.directive.ts @@ -0,0 +1,83 @@ +import { Directive, ElementRef, OnInit, Renderer2, inject, input } from '@angular/core'; + +/** + * `LetterWordCountDirective` is a directive that counts the number of letters or words in a given HTML element. + * It can also limit the maximum number of characters or words allowed in the element. + * It can also reverse the count to show the remaining number of characters or words. + * It can be used only with a textarea or input element. + * + * @directive + * @selector appLetterWordCount + * + * @example + * + * + */ +@Directive({ + selector: '[appLetterWordCount]', +}) +export class LetterWordCountDirective implements OnInit { + private readonly el = inject(ElementRef); + private readonly renderer = inject(Renderer2); + + /** Determines whether to count words or characters. If true, it counts words, otherwise it counts characters. */ + countWords = input(false); + + /** The target element to count the number of characters or words. */ + targetElement = input(undefined); + + /** The maximum number of characters or words allowed in the target element. */ + maxLimit = input(600); + + /** Determines whether to reverse the count. If true, it counts the remaining number of characters or words. */ + reverse = input(false); + + /** Determines whether to show the maximum limit of characters or words. */ + showMaxLimit = input(false); + + ngOnInit() { + const target = this.targetElement(); + if (target) { + this.renderer.listen(target, 'input', () => this.updateCount()); + } + this.updateCount(); + } + + private updateCount(): void { + const target = this.targetElement(); + if (!target) return; + + let text = ''; + if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) { + text = target.value; + } else { + text = target.innerText; + } + + const maxLimitValue = this.maxLimit(); + let count = this.countWords() ? text.split(/\s+/).filter((word) => word.length > 0).length : text.length; + if (this.reverse()) { + count = maxLimitValue - count; + if (count < 0) { + count = 0; + if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) { + target.value = text.substring(0, maxLimitValue); + } + } + } else if (count > maxLimitValue) { + this.renderer.setStyle(this.el.nativeElement, 'color', 'var(--red)'); + } else { + this.renderer.removeStyle(this.el.nativeElement, 'color'); + } + + this.el.nativeElement.innerText = `${count}`; + if (this.showMaxLimit()) { + this.el.nativeElement.innerText += ` / ${maxLimitValue}`; + } + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/directives/number-input.directive.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/directives/number-input.directive.ts new file mode 100644 index 000000000..c6e241d58 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/directives/number-input.directive.ts @@ -0,0 +1,50 @@ +import { Directive, ElementRef, OnInit, inject, input } from '@angular/core'; +import { NgControl } from '@angular/forms'; + +@Directive({ + selector: '[appNumberInput]', + host: { + '(input)': 'onInputChange($event)', + }, +}) +export class NumberInputDirective implements OnInit { + private readonly el = inject(ElementRef); + readonly ngControl = inject(NgControl, { optional: true, self: true }); + + type = input<'number' | 'phone' | 'zipcode'>('number'); + customRegex = input(undefined); + maxLength = input(14); + + private regex = new RegExp(/^\d+$/); + private computedMaxLength = 14; + + ngOnInit() { + this.computedMaxLength = this.maxLength(); + switch (this.type()) { + case 'number': + this.regex = new RegExp(/^\d+$/); + break; + case 'phone': + this.regex = new RegExp(this.customRegex() || /^(\+49)?[ ]?(\([0-9]{3}\)[ ]?)?[0-9]{3,14}$/); + this.computedMaxLength = 16; // +49 (123) 4567890 + break; + case 'zipcode': + this.regex = new RegExp(/^[0-9]{5}(?:-[0-9]{4})?$/); + this.computedMaxLength = 5; + break; + } + } + + onInputChange(event: Event) { + const initialValue = this.el.nativeElement.value; + this.el.nativeElement.value = initialValue.replace(/[^0-9 +()]/g, '').slice(0, this.computedMaxLength); + + if (this.ngControl?.control) { + if (!this.regex.test(this.el.nativeElement.value)) { + this.ngControl.control.setErrors({ invalid: true }); + } else { + this.ngControl.control.setErrors(null); + } + } + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/pipes/age.pipe.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/pipes/age.pipe.ts new file mode 100644 index 000000000..5659fce0d --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/pipes/age.pipe.ts @@ -0,0 +1,17 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'age', +}) +export class AgePipe implements PipeTransform { + transform(value: Date | string): number { + const today = new Date(); + const birthDate = new Date(value); + let age = today.getFullYear() - birthDate.getFullYear(); + const month = today.getMonth() - birthDate.getMonth(); + if (month < 0 || (month === 0 && today.getDate() < birthDate.getDate())) { + age--; + } + return age; + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/pipes/friendly-date.pipe.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/pipes/friendly-date.pipe.ts new file mode 100644 index 000000000..1159a1922 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/pipes/friendly-date.pipe.ts @@ -0,0 +1,48 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; + +@Pipe({ + name: 'friendlyDate', +}) +export class FriendlyDatePipe implements PipeTransform { + constructor(private readonly _translateService: TranslateService) {} + + transform(value: Date | string, forceYear = false): string { + if (!value) return '-'; + + if (typeof value === 'string') { + value = new Date(value); + } + + const today = new Date(); + const yesterday = new Date(today); + yesterday.setDate(yesterday.getDate() - 1); + + if (this._isSameDate(value, today)) { + return `${this._translateService.instant('Today')}, ${this._getFormattedDate(value)}`; + } else if (this._isSameDate(value, yesterday)) { + return `${this._translateService.instant('yesterday')}, ${this._getFormattedDate(value)}`; + } else { + if (value.getFullYear() === today.getFullYear() && value.getMonth() === today.getMonth()) { + return this._getFormattedDate(value, forceYear); + } else { + return this._getFormattedDate(value, true); + } + } + } + + private _isSameDate(date1: Date, date2: Date): boolean { + return date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate(); + } + + private _getFormattedDate(date: Date, includeYear = false): string { + const monthNamesShort = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + + const day = date.getDate(); + const monthIndex = date.getMonth(); + const month = this._translateService.instant(monthNamesShort[monthIndex]); + const year = includeYear ? date.getFullYear() : ''; + + return `${day} ${month}${includeYear ? ', ' + year : ''}`; + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/pipes/humanize.pipe.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/pipes/humanize.pipe.ts new file mode 100644 index 000000000..2d09de855 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/pipes/humanize.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { humanize } from '@core/utils'; + +@Pipe({ + name: 'humanize', +}) +export class HumanizePipe implements PipeTransform { + transform(value: any, caseSplit = false): any { + return humanize(value, caseSplit); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/pipes/index.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/pipes/index.ts new file mode 100644 index 000000000..5a2385ab7 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/pipes/index.ts @@ -0,0 +1,6 @@ +export * from './age.pipe'; +export * from './friendly-date.pipe'; +export * from './humanize.pipe'; +export * from './join-values.pipe'; +export * from './truncateText.pipe'; +export * from './timeAgo.pipe'; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/pipes/join-values.pipe.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/pipes/join-values.pipe.ts new file mode 100644 index 000000000..a6d554b38 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/pipes/join-values.pipe.ts @@ -0,0 +1,29 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'joinValues', +}) +/** + * Pipe to join an array of values into a single string. + * If the array contains objects, a propertyName can be provided to extract the value from each object. + * If no propertyName is provided, the array is joined as is. + * @example + *
{{ ['a', 'b', 'c'] | joinValues }}
+ *
{{ [{ name: 'a' }, { name: 'b' }, { name: 'c' }] | joinValues: 'name' }}
+ *
{{ [{ name: 'a' }, { name: 'b' }, { name: 'c' }] | joinValues: 'name': '-' }}
+ */ +export class JoinValuesPipe implements PipeTransform { + transform(value: any[], propertyName?: string, separator = ', '): string { + if (!value || value.length === 0) { + return ''; + } + + // Check if we're dealing with an array of objects and a propertyName is provided + if (propertyName && value.every((item) => typeof item === 'object' && item !== null && propertyName in item)) { + return value.map((item) => item[propertyName]).join(separator); + } + + // Default case for arrays of strings or numbers + return value.join(separator); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/pipes/timeAgo.pipe.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/pipes/timeAgo.pipe.ts new file mode 100644 index 000000000..7c93c57dc --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/pipes/timeAgo.pipe.ts @@ -0,0 +1,44 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { formatDate } from '@angular/common'; + +@Pipe({ + name: 'timeAgo', +}) +export class TimeAgoPipe implements PipeTransform { + constructor(private readonly _translateService: TranslateService) {} + + transform(value: string, ...args: unknown[]): string | null { + if (!value) return null; + + const date = new Date(value); + const now = new Date(); + const diffInMs = now.getTime() - date.getTime(); + const diffInSec = Math.floor(diffInMs / 1000); + const diffInMin = Math.floor(diffInSec / 60); + const diffInHours = Math.floor(diffInMin / 60); + const diffInDays = Math.floor(diffInHours / 24); + + if (diffInMin < 1) { + return this._translateService.instant('just now'); + } else if (diffInMin === 1) { + return this._translateService.instant('a minute ago'); + } else if (diffInMin < 60) { + return this._translateService.instant('minutes ago', { minutes: diffInMin }); + } else if (diffInHours === 1) { + return this._translateService.instant('an hour ago'); + } else if (diffInHours < 24) { + return this._translateService.instant('hours ago', { hours: diffInHours }); + } else if (diffInDays === 1) { + return this._translateService.instant('yesterday'); + } else if (diffInDays < 7) { + return this._translateService.instant('days ago', { days: diffInDays }); + } else if (diffInDays < 14) { + return this._translateService.instant('a week ago'); + } else if (diffInDays < 30) { + return this._translateService.instant('weeks ago', { weeks: Math.floor(diffInDays / 7) }); + } else { + return formatDate(date, 'dd/MM/yyyy', 'en-US'); + } + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/pipes/truncateText.pipe.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/pipes/truncateText.pipe.ts new file mode 100644 index 000000000..3377100c3 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/pipes/truncateText.pipe.ts @@ -0,0 +1,30 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { SafeHtml } from '@angular/platform-browser'; + +/** + * **Truncate text Pipe** + * @description Truncate text to a specified length, default is 100 characters, and add an ellipsis at the end. + * @param value: string + * @param limit: number + * @param completeWords: boolean + * @param ellipsis: string + * @returns string + * @example + * {{ 'This is a long text' | truncateText: 10: true: '...' }} + */ +@Pipe({ + name: 'truncateText', +}) +export class TruncateTextPipe implements PipeTransform { + transform(value: string, limit = 100, completeWords = false, ellipsis = '...'): SafeHtml { + if (!value) { + return ''; + } + let truncatedText = value; + if (completeWords) { + limit = value.substring(0, limit).lastIndexOf(' '); + } + truncatedText = value.length > limit ? value.substring(0, limit) + ellipsis : value; + return truncatedText; + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/services/index.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/services/index.ts new file mode 100644 index 000000000..224590da1 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shared/services/index.ts @@ -0,0 +1 @@ +export * from './bottomSheet.service'; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/components/header/header.component.html b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/components/header/header.component.html new file mode 100644 index 000000000..53dda3522 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/components/header/header.component.html @@ -0,0 +1,46 @@ + diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/components/header/header.component.scss b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/components/header/header.component.scss new file mode 100644 index 000000000..26494afeb --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/components/header/header.component.scss @@ -0,0 +1,172 @@ +.header-nav { + position: relative; + background-color: var(--white); + padding: 0.75rem 1.5rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +.header-left { + .app-title { + margin: 0; + font-size: 1.125rem; + font-weight: 600; + color: var(--text-color-dark, #1f2937); + } +} + +.header-right { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.icon-btn { + width: 40px; + height: 40px; + border-radius: 8px; + border: none; + background: transparent; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.25rem; + transition: background 0.2s; + + &:hover { + background: #f3f4f6; + } +} + +.notification-btn { + position: relative; + + .notification-badge { + position: absolute; + top: 4px; + right: 4px; + min-width: 18px; + height: 18px; + padding: 0 4px; + border-radius: 9999px; + background: #ef4444; + color: white; + font-size: 0.625rem; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + } +} + +.user-menu { + position: relative; + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem; + border-radius: 8px; + cursor: pointer; + margin-left: 0.5rem; + + &:hover { + background: #f3f4f6; + } +} + +.user-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + background: linear-gradient(135deg, #f97316 0%, #ea580c 100%); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 0.875rem; +} + +.user-name { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-color-dark, #1f2937); + + @media (max-width: 640px) { + display: none; + } +} + +.chevron { + font-size: 0.625rem; + color: var(--text-color-medium, #6b7280); + + @media (max-width: 640px) { + display: none; + } +} + +.dropdown-menu { + position: absolute; + top: calc(100% + 0.5rem); + right: 0; + min-width: 220px; + background: var(--white); + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + padding: 0.5rem 0; + z-index: 100; + + &.hidden { + display: none; + } +} + +.dropdown-header { + padding: 0.75rem 1rem; + + strong { + display: block; + font-size: 0.875rem; + color: var(--text-color-dark, #1f2937); + } + + small { + display: block; + font-size: 0.75rem; + color: var(--text-color-medium, #6b7280); + margin-top: 0.25rem; + } +} + +.dropdown-divider { + height: 1px; + background: #e5e7eb; + margin: 0.25rem 0; +} + +.dropdown-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.625rem 1rem; + cursor: pointer; + font-size: 0.875rem; + color: var(--text-color-dark, #1f2937); + text-decoration: none; + transition: background 0.2s; + + &:hover { + background: #f3f4f6; + } + + &.text-danger { + color: #dc2626; + } + + .item-icon { + font-size: 1rem; + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/components/header/header.component.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/components/header/header.component.ts new file mode 100644 index 000000000..4841a77e2 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/components/header/header.component.ts @@ -0,0 +1,59 @@ +import { Component, ChangeDetectionStrategy, inject, signal, computed } from '@angular/core'; +import { Router } from '@angular/router'; +import { CredentialsService } from '@app/auth/services/credentials.service'; +import { ThemeService } from '@app/shell/services/theme.service'; + +@Component({ + selector: 'app-header', + templateUrl: './header.component.html', + styleUrls: ['./header.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class HeaderComponent { + private readonly credentialsService = inject(CredentialsService); + private readonly themeService = inject(ThemeService); + private readonly router = inject(Router); + + readonly menuHidden = signal(true); + readonly isDarkTheme = signal(false); + + readonly user = computed(() => this.credentialsService.credentials()); + + readonly userInitials = computed(() => { + const user = this.user(); + if (!user) return '?'; + const first = user.firstName?.charAt(0) || ''; + const last = user.lastName?.charAt(0) || ''; + return (first + last).toUpperCase() || '?'; + }); + + toggleMenu() { + this.menuHidden.update((hidden) => !hidden); + } + + toggleTheme() { + this.isDarkTheme.update((dark) => { + const newValue = !dark; + if (newValue) { + this.themeService.setDarkTheme(); + } else { + this.themeService.setLightTheme(); + } + return newValue; + }); + } + + navigateToProfile() { + this.router.navigate(['/profile']); + this.menuHidden.set(true); + } + + navigateToSettings() { + this.router.navigate(['/settings']); + this.menuHidden.set(true); + } + + logout() { + this.router.navigate(['/logout']); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/components/sidebar/sidebar.component.html b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/components/sidebar/sidebar.component.html new file mode 100644 index 000000000..d2ca17e3e --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/components/sidebar/sidebar.component.html @@ -0,0 +1,86 @@ + diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/components/sidebar/sidebar.component.scss b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/components/sidebar/sidebar.component.scss new file mode 100644 index 000000000..69a5d14e8 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/components/sidebar/sidebar.component.scss @@ -0,0 +1,330 @@ +nav { + transition: width 0.2s ease-in-out; + width: 248px; + background-color: var(--deep-gray); + height: 100vh; + overflow-y: auto; + overflow-x: hidden; + padding: 30px 0; + -ms-overflow-style: none; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + /* for Chrome, Safari, and Opera */ + } + + .toggle-icon { + position: absolute; + top: 63px; + right: -10px; + cursor: pointer; + z-index: 100; + transition: transform 0.3s ease-in-out; + transform: rotate(180deg); + border-radius: 50%; + background: var(--gradient-primary-light); + width: 24px; + height: 24px; + display: flex; + justify-content: center; + align-items: center; + + &.open { + transform: rotate(0deg); + } + + &:hover { + background: var(--dull-orange); + } + } + + .logo-container { + display: flex; + justify-content: flex-start; + align-items: center; + margin-bottom: 43px; + margin-left: 26px; + cursor: pointer; + + .logo { + width: 30px; + height: 30px; + transition: width 0.3s ease-in-out; + } + + h4 { + color: var(--white); + margin-left: 10px; + transition: opacity 0.3s ease-in-out; + } + } + + .navbar-menu-items { + display: flex; + flex-wrap: nowrap; + flex-direction: column; + justify-content: center; + align-content: center; + + .menu { + margin-bottom: 15px; + margin-left: 18px; + margin-right: 18px; + + &:hover { + background-color: rgba(255, 255, 255, 0.04); + border-radius: 9px; + } + + .active { + background-color: rgba(255, 255, 255, 0.04); + border-radius: 9px; + color: var(--gray-400); + //color: var(--dull-orange); + + span { + color: var(--gray-400) !important; + } + } + + .menu-item { + display: flex; + justify-content: space-between; + flex-wrap: nowrap; + padding: 10px; + cursor: pointer; + min-width: 200px; + + &.disabled { + cursor: not-allowed; + opacity: 0.3; + } + + .item { + display: flex; + + span { + width: 145px; + overflow: hidden; + text-overflow: ellipsis; + word-wrap: normal; + word-break: keep-all; + white-space: nowrap; + transition: + opacity 0.3s ease-in-out, + transform 0.3s ease-in-out; + opacity: 1; + font-size: 14px; + font-weight: 600; + color: var(--sidebar-icon-color); + transform: translateX(0); + margin-left: 14px; + margin-bottom: 2px; + } + } + + .submenu-icon { + &.open { + transform: rotate(180deg); + } + } + + .sub-menu-items { + li { + padding: 10px; + transition: background-color 0.3s ease-in-out; + + &:hover { + background-color: lightgray; + } + + app-icon { + display: inline-block; + transition: color 0.3s ease-in-out; + } + + a { + span { + // Default styles for submenu item text + width: 80px; + overflow: hidden; + text-overflow: ellipsis; + word-wrap: normal; + white-space: nowrap; + } + } + } + + .disabled { + cursor: not-allowed !important; + opacity: 0.3 !important; + } + } + } + + .sub-menu-items { + li { + padding: 10px; + min-width: 200px; + + app-icon { + display: inline-block; + transition: color 0.3s ease-in-out; + } + + a { + //margin-left: 30px; + text-decoration: none; + padding: 5px 0px 5px 20px; + display: block; + word-wrap: normal; + white-space: nowrap; + cursor: pointer; + + span { + font-size: 14px; + font-style: normal; + font-weight: 600; + color: var(--gray-600); + margin-left: 12px; + width: 140px; + overflow: hidden; + text-overflow: ellipsis; + word-wrap: normal; + white-space: nowrap; + display: inline-block; + margin-bottom: -5px; + } + + &.active { + } + } + } + + .disabled { + cursor: not-allowed !important; + opacity: 0.6 !important; + } + } + } + } + + &.squeeze { + width: 80px; + + .logo-container { + h4 { + opacity: 0; + } + } + + .navbar-menu-items { + .menu { + .menu-item { + min-width: auto; + + .item { + span { + word-break: keep-all; + white-space: nowrap; + opacity: 0; + transform: translateX(-20px); + display: none; + } + } + + .submenu-icon { + display: none; + } + } + + .sub-menu-items { + li { + width: 50px; + min-width: 50px; + + a { + margin-left: -5px; + padding: 8px; + min-width: auto; + cursor: pointer; + + span { + word-break: keep-all; + white-space: nowrap; + opacity: 0; + transform: translateX(-20px); + display: none; // Hide text in squeezed mode + } + } + } + } + } + } + + footer { + .store-icons { + opacity: 0; + } + + .app-version { + opacity: 0; + } + } + } + + .divider { + display: block; + height: 1px; + background: linear-gradient(270deg, rgba(97, 98, 100, 0) 0%, #616264 51.38%, rgba(97, 98, 100, 0) 100%); + } + + footer { + position: absolute; + bottom: 0; + width: 100%; + background-color: var(--deep-gray); + padding: 15px 2px; + overflow: hidden; + + .store-icons { + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 30px; + transition: opacity 0.3s ease-in-out; + opacity: 1; + + img { + width: 100px; + height: 30px; + } + } + + .app-version { + color: var(--sidebar-icon-color); + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 16px; + text-align: center; + width: 130px; + word-wrap: normal; + white-space: nowrap; + margin: auto; + opacity: 1; + transition: opacity 0.3s ease-in-out; + + small { + font-size: 10px; + } + } + } +} + +// Aux Class + +.pb { + padding-bottom: 110px; +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/components/sidebar/sidebar.component.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/components/sidebar/sidebar.component.ts new file mode 100644 index 000000000..18c29cdfa --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/components/sidebar/sidebar.component.ts @@ -0,0 +1,74 @@ +import { Component, OnInit, ChangeDetectionStrategy, inject, signal, DestroyRef } from '@angular/core'; +import { NavigationEnd, Router, RouterLink, RouterLinkActive } from '@angular/router'; +import { NgOptimizedImage } from '@angular/common'; +import { TranslateModule } from '@ngx-translate/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { filter } from 'rxjs/operators'; +import { environment } from '@env/environment'; +import { NavMode, ShellService } from '@app/shell/services/shell.service'; +import { webSidebarMenuItems } from '@core/constants'; +import { CredentialsService } from '@app/auth/services/credentials.service'; +import { NavMenuItem } from '@core/interfaces'; + +@Component({ + selector: 'app-sidebar', + templateUrl: './sidebar.component.html', + styleUrls: ['./sidebar.component.scss'], + imports: [RouterLink, RouterLinkActive, TranslateModule, NgOptimizedImage], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SidebarComponent implements OnInit { + private readonly router = inject(Router); + private readonly credentialsService = inject(CredentialsService); + private readonly destroyRef = inject(DestroyRef); + readonly shellService = inject(ShellService); + + readonly version = environment.version; + readonly year = environment.buildYear ?? 2024; + sidebarItems: NavMenuItem[] = webSidebarMenuItems; + readonly sidebarExtendedItem = signal(-1); + readonly navExpanded = signal(true); + + ngOnInit(): void { + this.shellService.activeNavTab(this.sidebarItems, this.sidebarExtendedItem()); + + this.router.events + .pipe( + takeUntilDestroyed(this.destroyRef), + filter((event) => event instanceof NavigationEnd), + ) + .subscribe(() => { + this.shellService.activeNavTab(this.sidebarItems, this.sidebarExtendedItem()); + }); + + this.shellService.navMode$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((mode) => { + this.navExpanded.set(mode === NavMode.Free); + }); + } + + toggleSidebar(isEnterEvent: boolean): void { + const mode = this.shellService.navMode(); + if (isEnterEvent) { + this.navExpanded.set(true); + } else if (!isEnterEvent && mode === NavMode.Free) { + this.navExpanded.set(false); + } + } + + activateSidebarItem(index: number): void { + const item = this.sidebarItems[index]; + if (item.disabled) return; + + if (index !== this.sidebarExtendedItem()) { + this.sidebarExtendedItem.set(index); + } else { + this.sidebarExtendedItem.set(-1); + } + + this.shellService.activateNavItem(index, this.sidebarItems); + } + + activateSidebarSubItem(index: number, subItem: NavMenuItem): void { + this.shellService.activateNavSubItem(index, subItem, this.sidebarItems); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/services/shell.service.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/services/shell.service.ts new file mode 100644 index 000000000..15d9ad5a2 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/services/shell.service.ts @@ -0,0 +1,150 @@ +import { Route, Router, Routes } from '@angular/router'; + +import { AuthenticationGuard, PERMISSIONS, PermissionService } from '@app/auth'; +import { ShellComponent } from '@app/shell/shell.component'; +import { inject, Injectable, signal } from '@angular/core'; +import { toObservable } from '@angular/core/rxjs-interop'; +import { NavMenuItem } from '@core/interfaces'; + +/** + * Provides helper methods to create routes. + */ +export class Shell { + /** + * Creates routes using the shell component and authentication. + * @param routes The routes to add. + * @return The new route using shell as the base. + */ + static childRoutes(routes: Routes): Route { + return { + path: '', + component: ShellComponent, + children: routes, + canActivate: [AuthenticationGuard], + + data: { reuse: true }, + }; + } +} + +@Injectable({ + providedIn: 'root', +}) +export class ShellService { + private readonly router = inject(Router); + private readonly permissionService = inject(PermissionService); + + /** Current navigation mode signal */ + readonly navMode = signal(NavMode.Free); + + /** Observable bridge for legacy consumers */ + readonly navMode$ = toObservable(this.navMode); + + allowedAccess(item: NavMenuItem): boolean { + if (item.roles && item.roles.length) { + return item.roles.includes(this.permissionService.userRole()); + } + + if (item.permissions && item.permissions.length) { + return item.permissions.some((permission: PERMISSIONS) => this.permissionService.hasPermission(permission)); + } + + return true; + } + + toggleNavMode(): void { + this.navMode.update((mode) => (mode === NavMode.Free ? NavMode.Locked : NavMode.Free)); + } + + activeNavTab(items: NavMenuItem[], extendedItem: number): void { + items.forEach((item, index) => { + if (item.href) { + const urlSegments = this.router.url.split('/').filter((segment) => segment.length > 0); + const hrefSegments = item.href.split('/').filter((segment) => segment.length > 0); + const isActive = hrefSegments.every((segment, i) => segment === urlSegments[i]); + + item.active = isActive; + + if (isActive && extendedItem) { + extendedItem = index; + } + + if (item.subItems) { + item.subItems.forEach((subItem) => { + if (subItem.href) { + const subItemHrefSegments = subItem.href.split('/').filter((segment) => segment.length > 0); + subItem.active = subItemHrefSegments.every((segment, i) => segment === urlSegments[i]); + } + }); + } + } else { + item.active = false; + } + }); + } + + activateNavItem(index: number, navItems: NavMenuItem[]): void { + const item = navItems[index]; + if (item.disabled) return; + + // Use requestAnimationFrame for DOM operations (zoneless-compatible) + requestAnimationFrame(() => { + const element = document.getElementById(`menu-item-${index}`); + const navElement = document.querySelector('nav'); + + if (element && navElement) { + const elementRect = element.getBoundingClientRect(); + const navRect = navElement.getBoundingClientRect(); + + const relativeTop = elementRect.top - navRect.top; + const desiredScrollPosition = navElement.scrollTop + relativeTop - navRect.height / 2; + + navElement.scrollTo({ top: desiredScrollPosition, behavior: 'smooth' }); + } + }); + + if (item && (!item.subItems || !item.subItems.length)) { + this.router.navigate([item.href]); + } else { + // set false to all subitems of all items + navItems.forEach((item) => { + if (item.subItems) { + item.subItems.forEach((subItem) => { + subItem.active = false; + }); + } + }); + } + } + + activateNavSubItem(i: number, subItem: NavMenuItem, sidebarItems: NavMenuItem[]) { + if (subItem.disabled) return; + subItem.active = true; + sidebarItems[i].active = true; + // disable all other subitems + sidebarItems[i].subItems.forEach((item) => { + if (item !== subItem) { + item.active = false; + } + }); + if (subItem.href) { + this.router.navigate([subItem.href]); + } + + if (subItem.url) { + window.open(subItem.url, '_blank'); + } + } + + getCurrentActiveRoute(lastSegmentOnly = true): string { + const url = this.router.url; + const urlSegments = url.split('/'); + const lastSegment = urlSegments[urlSegments.length - 1]; + return lastSegmentOnly ? lastSegment : url; + } +} + +export enum NavMode { + Locked, + Free, +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/services/theme.service.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/services/theme.service.ts new file mode 100644 index 000000000..cd4ad80c0 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/services/theme.service.ts @@ -0,0 +1,77 @@ +import { Injectable } from '@angular/core'; + +/* The ThemeService class provides methods to set light and dark theme colors by updating +CSS variables in the document root element. */ +@Injectable({ + providedIn: 'root', +}) +export class ThemeService { + private readonly _lightThemeColors: { [key: string]: string } = { + // Primary theme colors + '--orange': '#E9380B', + '--yellow': '#FBC52D', + '--dark-charcoal': '#333843', + '--deep-gray': '#393B3D', + '--slate-gray': '#667085', + '--surface-1': '#FAFAFA', + '--text-color-light': '#ffffff', + '--text-color-medium': '#667085', + '--text-color-dark': '#333843', + // Grayscale + '--white': '#fff', + '--gray-100': '#f8f9fa', + '--gray-200': '#e9ecef', + '--gray-300': '#dee2e6', + '--gray-400': '#ced4da', + '--gray-500': '#adb5bd', + '--gray-600': '#868e96', + '--gray-700': '#495057', + '--gray-800': '#343a40', + '--gray-900': '#212529', + '--black': '#000', + // ... Add any additional light theme colors + }; + + private readonly _darkThemeColors: { [key: string]: string } = { + // Adjusted primary theme colors for dark theme + '--orange': '#D15F41', + '--yellow': '#C2A44D', + '--dark-charcoal': '#2B2F33', + '--deep-gray': '#2F3134', + '--slate-gray': '#4A5059', + '--surface-1': '#202124', + '--text-color-light': '#212529', + '--text-color-medium': '#ced4da', + '--text-color-dark': '#f8f9fa', + // Inverted Grayscale for Dark Theme + '--white': '#212529', + '--gray-100': '#343a40', + '--gray-200': '#495057', + '--gray-300': '#868e96', + '--gray-400': '#adb5bd', + '--gray-500': '#ced4da', + '--gray-600': '#dee2e6', + '--gray-700': '#e9ecef', + '--gray-800': '#f8f9fa', + '--gray-900': '#fff', + '--black': '#f8f9fa', + // ... Add any additional dark theme colors + }; + + constructor() {} + + setLightTheme(): void { + this._setTheme(this._lightThemeColors); + } + + setDarkTheme(): void { + this._setTheme(this._darkThemeColors); + } + + private _setTheme(colors: Record): void { + const root = document.documentElement; + Object.keys(colors).forEach((key) => { + root.style.setProperty(key, colors[key]); + }); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/shell.component.html b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/shell.component.html new file mode 100644 index 000000000..cbbf6a675 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/shell.component.html @@ -0,0 +1,13 @@ +
+ +
+
+ +
+
+ +
+
+
diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/shell.component.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/shell.component.ts new file mode 100644 index 000000000..a008b342e --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/app/shell/shell.component.ts @@ -0,0 +1,29 @@ +import { Component, ChangeDetectionStrategy, inject, signal } from '@angular/core'; +import { Router, RouterOutlet } from '@angular/router'; +import { ShellService } from '@app/shell/services/shell.service'; +import { SidebarComponent } from './components/sidebar/sidebar.component'; +import { HeaderComponent } from './components/header/header.component'; + +@Component({ + selector: 'app-shell', + templateUrl: './shell.component.html', + imports: [RouterOutlet, SidebarComponent, HeaderComponent], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ShellComponent { + private readonly shellService = inject(ShellService); + private readonly router = inject(Router); + + readonly isSidebarActive = signal(false); + + sidebarToggle(toggleState: boolean) { + this.isSidebarActive.set(toggleState); + } + + private reloadCurrentRoute(path?: string) { + const currentUrl = path || this.router.url; + this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { + this.router.navigate([currentUrl]); + }); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/environments/.env.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/environments/.env.ts new file mode 100644 index 000000000..9e99cadec --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/environments/.env.ts @@ -0,0 +1,3 @@ +export const env: { [s: string]: string | null } = { + npm_package_version: '0.0.0', +}; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/environments/environment.prod.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/environments/environment.prod.ts new file mode 100644 index 000000000..10f9ff7e0 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/environments/environment.prod.ts @@ -0,0 +1,9 @@ +import { env } from './.env'; + +export const environment = { + production: true, + version: env['npm_package_version'] + '-dev', + defaultLanguage: 'de-DE', + supportedLanguages: ['de-DE', 'en-US', 'es-ES', 'fr-FR', 'it-IT'], + buildYear: 2024, +}; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/environments/environment.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/environments/environment.ts new file mode 100644 index 000000000..6704e0e03 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/environments/environment.ts @@ -0,0 +1,9 @@ +import { env } from './.env'; + +export const environment = { + production: false, + version: env['npm_package_version'] + '-dev', + defaultLanguage: 'en-US', + supportedLanguages: ['en-US'], + buildYear: 2024, +}; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/index.html b/apps/error-tracking/no-posthog-angular/angular-saas/src/index.html new file mode 100644 index 000000000..c9084cf8c --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/index.html @@ -0,0 +1,16 @@ + + + + + NewAngularBoilerplate + + + + + + + + + + + diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/main.ts b/apps/error-tracking/no-posthog-angular/angular-saas/src/main.ts new file mode 100644 index 000000000..d8e74ad92 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/main.ts @@ -0,0 +1,7 @@ +/// + +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { AppComponent } from './app/app.component'; + +bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)); diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/styles.scss b/apps/error-tracking/no-posthog-angular/angular-saas/src/styles.scss new file mode 100644 index 000000000..5d9ff61be --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/styles.scss @@ -0,0 +1,30 @@ +/* + * Entry point of global application style. + * Component-specific style should not go here and be included directly as part of the components. + */ +@use "@ngxpert/hot-toast/src/styles/styles.scss"; + +// Sanitize.css +@use "sanitize.css/sanitize.css"; + +// Theme variables +@use "theme/variables"; + +// Base and layout styles +@use "theme/base"; +@use "theme/layout"; + +// 3rd party libraries +@use "bootstrap/scss/bootstrap-grid"; + +// Mixins +@use "theme/mixins"; + +// Overrides +@use "theme/overrides"; + +// Utilities +@use "theme/utilities"; + +// theme customization +@use "theme/theme"; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/theme/_base.scss b/apps/error-tracking/no-posthog-angular/angular-saas/src/theme/_base.scss new file mode 100644 index 000000000..cfa34cf59 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/theme/_base.scss @@ -0,0 +1,80 @@ +/* + * Sets up base styles for the application like body background, font family, etc. + */ + +// font import + +@font-face { + font-family: "Manrope"; + src: url("/fonts/Manrope-Regular.ttf") format("truetype"); + font-weight: 400; // Regular weight + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "Manrope"; + src: url("/fonts/Manrope-Medium.ttf") format("truetype"); + font-weight: 500; // Medium weight + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "Manrope"; + src: url("/fonts/Manrope-SemiBold.ttf") format("truetype"); + font-weight: 600; // SemiBold weight + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "Manrope"; + src: url("/fonts/Manrope-Bold.ttf") format("truetype"); + font-weight: 700; // Bold weight + font-style: normal; + font-display: swap; +} + +* { + padding: 0; + margin: 0; + font-family: "Manrope", sans-serif; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + background-color: var(--surface-1); +} + +*::-webkit-scrollbar, +body::-webkit-scrollbar { + width: 18px; + height: 18px; +} + +*::-webkit-scrollbar-track, +body::-webkit-scrollbar-track { + background-color: #f2f2f2; + border-radius: 20px; + border: 6px solid transparent; + background-clip: content-box; +} + +*::-webkit-scrollbar-thumb, +body::-webkit-scrollbar-thumb { + background-color: #cfd0d1; + border-radius: 20px; + border: 6px solid transparent; + background-clip: content-box; +} + +*[routerLink] { + cursor: pointer !important; +} + +*:focus-visible { + outline: 1px solid var(--deep-slate-gray); +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/theme/_layout.scss b/apps/error-tracking/no-posthog-angular/angular-saas/src/theme/_layout.scss new file mode 100644 index 000000000..2f24d42bc --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/theme/_layout.scss @@ -0,0 +1,62 @@ +/* + * Contains styles specific to the overall app layout and components like headers and footers + */ + +.shell.layout-wrapper { + display: flex; + height: 100vh; + width: 100vw; + overflow: hidden; + position: relative; + + aside { + position: relative; + } + + main { + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; + + header.main-header { + -webkit-box-shadow: 0 1px 5px -3px rgba(173, 171, 173, 0.5); + -moz-box-shadow: 0 1px 5px -3px rgba(173, 171, 173, 0.5); + box-shadow: 0 1px 5px -3px rgba(173, 171, 173, 0.5); + z-index: 30; + } + + .content { + flex: 1; + overflow: auto; + z-index: 20; + } + + .side-nav { + position: fixed; + bottom: 0; + width: 100%; + margin: 0 auto; + left: 0; + right: 0; + } + } + + &.overlay { + &::after { + width: 100%; + height: 100%; + content: ""; + position: absolute; + right: 0; + top: 0; + background-color: rgba(#393b3d, 0.4); + z-index: 200; + } + } +} + +.main-container { + padding: 24px; + background: var(--surface-1); +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/theme/_mixins.scss b/apps/error-tracking/no-posthog-angular/angular-saas/src/theme/_mixins.scss new file mode 100644 index 000000000..8eff9a8cb --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/theme/_mixins.scss @@ -0,0 +1,3 @@ +/* + * Dedicated file for custom SCSS mixins. + */ diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/theme/_overrides.scss b/apps/error-tracking/no-posthog-angular/angular-saas/src/theme/_overrides.scss new file mode 100644 index 000000000..75667c045 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/theme/_overrides.scss @@ -0,0 +1,3 @@ +/* + * Dedicated file for overriding styles from third-party libraries like Bootstrap, ng-select, ngneat dialog, ngneat tippy. + */ diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/theme/_utilities.scss b/apps/error-tracking/no-posthog-angular/angular-saas/src/theme/_utilities.scss new file mode 100644 index 000000000..5af4bdfb1 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/theme/_utilities.scss @@ -0,0 +1,3 @@ +/* + * Dedicated file for custom utility classes. like margin, padding, display, etc. + */ diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/theme/_variables.scss b/apps/error-tracking/no-posthog-angular/angular-saas/src/theme/_variables.scss new file mode 100644 index 000000000..8bf437cca --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/theme/_variables.scss @@ -0,0 +1,88 @@ +/* + * Application global variables. + */ + +// Light Theme Colors +:root { + // Colors + --light-teal: #3dc19a; + --dark-teal: #44a085; + --dark-blue: #3e71bd; + --orange: #e9380b; + --light-orange: #ec5c37; + --dull-orange: #dd5741; + --orange-opaque: rgba(233, 56, 11, 0.12); + --teal-opaque: rgba(61, 193, 154, 0.2); + --blue-opaque: rgba(62, 113, 189, 0.2); + --yellow: #fbc52d; + --dark-yellow: #ebb106; + --deep-yellow: #fec41a; + --yellow-opaque: rgba(235, 177, 6, 0.14); + --slate-gray: #667085; + --dull-gray: #ffffff80; + --dark-charcoal: #333843; + --deep-gray: #393b3d; + --deep-slate-gray: #323338; + --red: #dc3545; + --green: #389f6e; + + // Theme Colors + --surface-1: #fafafa; + --text-color-light: #ffffff; + --text-color-medium: #667085; + --text-color-dark: #333843; + --sidebar-icon-color: #787878; + --icon-hover: #667085; + + // Grayscale + + --white: #fff; + --gray: #e1e3e6; + --gray-100: #f8f9fa; + --gray-150: #f1f3f4; + --gray-200: #e9ecef; + --gray-300: #dee2e6; + --gray-400: #ced4da; + --gray-500: #adb5bd; + --gray-600: #868e96; + --gray-700: #495057; + --gray-800: #343a40; + --gray-900: #212529; + --black: #000; + --minor-gray: #f2f2f5; + + // Gradient for Light Theme + --gradient-primary-light: linear-gradient(173deg, #e73609 -96.7%, #e9496e 17.35%, #ffc51a 111.15%); + --gradient-primary-light-opaque: linear-gradient(173deg, rgba(231, 54, 9, 0.6) -96.7%, rgba(233, 73, 110, 0.6) 17.35%, rgba(255, 197, 26, 0.6) 111.15%); + --shadow-light: 0px 6px 19px 0px rgba(120, 120, 120, 0.19); + + // Status Colors (SaaS) + --success: #10b981; + --warning: #f59e0b; + --error: #ef4444; + --info: #3b82f6; + + // Spacing + --spacing-xs: 0.25rem; + --spacing-sm: 0.5rem; + --spacing-md: 1rem; + --spacing-lg: 1.5rem; + --spacing-xl: 2rem; + + // Border Radius + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-full: 9999px; +} + +$blue: #0073dd; +$indigo: #6610f2; +$purple: #6f42c1; +$pink: #e83e8c; +$red: #dc3545; +$orange: #fd7e14; +$yellow: #ffc107; +$green: #28a745; +$teal: #20c997; +$cyan: #17a2b8; diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/theme/theme.scss b/apps/error-tracking/no-posthog-angular/angular-saas/src/theme/theme.scss new file mode 100644 index 000000000..6210f538f --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/theme/theme.scss @@ -0,0 +1,224 @@ +/* + * Global application theme. + * All global classes and theme styles for html elements and classes should be defined here. + */ + +// For Example +// Custom Input Fields Global Styles +label { + display: block; + color: var(--text-color-dark); + font-size: 14px; + font-style: normal; + font-weight: 600; + line-height: normal; +} + +input[type="text"], +input[type="email"], +input[type="tel"], +input[type="number"], +input[type="password"], +input[type="date"], +input[type="search"], +input[type="file"], +input[type="url"], +input[type="time"], +textarea { + width: 100%; + padding: 8px 16px; + font-size: 14px; + font-weight: 500; + color: var(--text-color-medium); + outline: none; + background-color: var(--white); + border: 1px solid var(--gray); + box-sizing: border-box; + //box-shadow: 0px 0px 6px rgba(23, 43, 77, 0.04); + border-radius: 6px; +} + +input[type="text"]:focus, +input[type="email"]:focus, +input[type="tel"]:focus, +input[type="number"]:focus, +input[type="password"]:focus, +input[type="date"]:focus, +input[type="search"]:focus, +input[type="file"]:focus, +input[type="url"]:focus, +input[type="time"]:focus, +textarea:focus { + border: 1px solid var(--deep-slate-gray); + outline: none; +} + +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type="number"] { + -moz-appearance: textfield; +} + +textarea { + overflow-y: scroll; + overflow-x: hidden; + max-height: 200px; +} + +// set color of all text elements +label, +p, +span, +a, +h1, +h2, +h3, +h4, +h5, +h6 { + color: var(--text-color-dark); +} + +// set style to all disabled text elements +label[disabled], +p[disabled], +span[disabled], +a[disabled], +li[disabled], +h1[disabled], +h2[disabled], +h3[disabled], +h4[disabled], +h5[disabled], +h6[disabled] { + color: var(--gray) !important; + pointer-events: none; + cursor: default; +} + +// Button Styles +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + border-radius: var(--radius-md); + font-weight: 600; + font-size: 0.875rem; + cursor: pointer; + transition: all 0.2s ease; + border: none; + text-decoration: none; + + &-primary { + background: var(--gradient-primary-light); + color: var(--white); + + &:hover { + opacity: 0.9; + } + } + + &-outline { + background: transparent; + border: 1px solid var(--gray-300); + color: var(--text-color-dark); + + &:hover { + background: var(--gray-100); + } + } + + &-ghost { + background: transparent; + color: var(--text-color-medium); + + &:hover { + background: var(--gray-100); + } + } + + &-danger { + background: var(--error); + color: var(--white); + + &:hover { + opacity: 0.9; + } + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +// Form Utilities +.form-group { + margin-bottom: var(--spacing-md); + + label { + margin-bottom: var(--spacing-sm); + } +} + +.form-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: var(--spacing-md); +} + +// Card Styles +.card { + background: var(--white); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-light); + overflow: hidden; + + &-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--spacing-md) var(--spacing-lg); + border-bottom: 1px solid var(--gray-200); + } + + &-body { + padding: var(--spacing-lg); + } +} + +// Status Badge Styles +.badge { + display: inline-flex; + align-items: center; + padding: 0.25rem 0.75rem; + border-radius: var(--radius-full); + font-size: 0.75rem; + font-weight: 600; + + &-success { + background: rgba(16, 185, 129, 0.1); + color: var(--success); + } + + &-warning { + background: rgba(245, 158, 11, 0.1); + color: var(--warning); + } + + &-error { + background: rgba(239, 68, 68, 0.1); + color: var(--error); + } + + &-info { + background: rgba(59, 130, 246, 0.1); + color: var(--info); + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/src/translations/en-US.json b/apps/error-tracking/no-posthog-angular/angular-saas/src/translations/en-US.json new file mode 100644 index 000000000..375ee1aa2 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/src/translations/en-US.json @@ -0,0 +1,11 @@ +{ + "APP_NAME": "Angular v18 Boiler Plate", + "LANGUAGE": "English", + "Hello World": "Hello World", + "Home": "Home", + "Login": "Login", + "Username": "Username", + "Password": "Password", + "Angular Boiler Plate V18": "Angular Boiler Plate V18", + "Dashboard": "Dashboard" +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/tsconfig.app.json b/apps/error-tracking/no-posthog-angular/angular-saas/tsconfig.app.json new file mode 100644 index 000000000..6e70d4307 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/tsconfig.app.json @@ -0,0 +1,11 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": ["@angular/localize"] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/tsconfig.json b/apps/error-tracking/no-posthog-angular/angular-saas/tsconfig.json new file mode 100644 index 000000000..d24e01c87 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/tsconfig.json @@ -0,0 +1,45 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "declaration": false, + "moduleResolution": "bundler", + "experimentalDecorators": true, + "importHelpers": true, + "noImplicitAny": false, + "skipLibCheck": true, + "isolatedModules": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "typeRoots": ["node_modules/@types"], + "baseUrl": "./", + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022", "dom"], + "paths": { + "@app/*": ["src/app/*"], + "@auth": ["src/app/auth"], + "@core/*": ["src/app/@core/*"], + "@pages/*": ["src/app/pages/*"], + "@shared/*": ["src/app/shared/*"], + "@env/*": ["src/environments/*"] + }, + "useDefineForClassFields": false + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "fullTemplateTypeCheck": true, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": false, + "preserveWhitespaces": true + } +} diff --git a/apps/error-tracking/no-posthog-angular/angular-saas/tsconfig.spec.json b/apps/error-tracking/no-posthog-angular/angular-saas/tsconfig.spec.json new file mode 100644 index 000000000..df03d3439 --- /dev/null +++ b/apps/error-tracking/no-posthog-angular/angular-saas/tsconfig.spec.json @@ -0,0 +1,10 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": ["jasmine", "@angular/localize"] + }, + "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/apps/error-tracking/no-posthog-django/django3-saas/.env.example b/apps/error-tracking/no-posthog-django/django3-saas/.env.example new file mode 100644 index 000000000..b8e3ce5c6 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/.env.example @@ -0,0 +1,21 @@ +# Django settings +SECRET_KEY=your-secret-key-here +DEBUG=True +ALLOWED_HOSTS=localhost,127.0.0.1 + +# Database (defaults to SQLite if not set) +DATABASE_URL= + +# Email settings (defaults to console backend for development) +EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend +EMAIL_HOST= +EMAIL_PORT=587 +EMAIL_HOST_USER= +EMAIL_HOST_PASSWORD= +EMAIL_USE_TLS=True +DEFAULT_FROM_EMAIL=noreply@example.com + +# Stripe settings (optional - for payment processing) +STRIPE_PUBLIC_KEY= +STRIPE_SECRET_KEY= +STRIPE_WEBHOOK_SECRET= diff --git a/apps/error-tracking/no-posthog-django/django3-saas/.gitignore b/apps/error-tracking/no-posthog-django/django3-saas/.gitignore new file mode 100644 index 000000000..be9c7d9b8 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/.gitignore @@ -0,0 +1,29 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +.venv/ +venv/ +ENV/ + +# Django +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal +*.pot +*.pyc + +# Static files +staticfiles/ + +# Environment +.env + +# IDE +.idea/ +.vscode/ +*.swp +*.swo diff --git a/apps/error-tracking/no-posthog-django/django3-saas/README.md b/apps/error-tracking/no-posthog-django/django3-saas/README.md new file mode 100644 index 000000000..6e117bfca --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/README.md @@ -0,0 +1,131 @@ +# Django SaaS example app + +A Django 3.0+ SaaS application for testing PostHog wizard integration. This app provides subscription billing, user authentication, and project management features. + +## Running the app + +### Prerequisites + +- Python 3.10+ +- SQLite (included with Python, used by default) +- Stripe account (optional, app runs in demo mode without it) + +### Installation + +1. Create and activate a virtual environment: + +```bash +python -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate +``` + +1. Install dependencies: + +```bash +pip install -r requirements.txt +``` + +1. Set up environment variables (create a `.env` file): + +```bash +SECRET_KEY=your-secret-key +# DATABASE_URL=postgresql://... # Optional, defaults to SQLite +STRIPE_PUBLIC_KEY=pk_test_... # Optional, enables Stripe +STRIPE_SECRET_KEY=sk_test_... # Optional, enables Stripe +STRIPE_WEBHOOK_SECRET=whsec_... # Optional, for webhooks +``` + +> **Note:** By default, the app uses SQLite (`db.sqlite3`) and runs in demo mode without Stripe. No additional setup is required for local development. + +1. Initialize the database: + +```bash +python manage.py migrate +python manage.py seed_plans # Optional: seed pricing plans +``` + +1. Run the development server: + +```bash +python manage.py runserver +``` + +The app will be available at `http://127.0.0.1:8000`. + +--- + +## Application structure + +``` +├── manage.py # Django management script +├── requirements.txt # Python dependencies +├── accounts/ # User authentication & profiles +│ ├── models.py # Custom User model +│ ├── views.py # Login, register, password reset +│ └── forms.py # Auth forms +├── billing/ # Subscription & payment handling +│ ├── models.py # Plan, Subscription models +│ ├── views.py # Stripe checkout, webhooks, billing portal +│ ├── admin.py # Django admin customization +│ └── management/ # seed_plans command +├── config/ # Django settings +│ ├── settings.py +│ ├── urls.py +│ └── wsgi.py +├── dashboard/ # Main app functionality +│ ├── models.py # Project, ActivityLog models +│ ├── views.py # Dashboard, project CRUD +│ └── forms.py +├── marketing/ # Public pages +│ └── views.py # Home, features pages +├── static/ # CSS, JS, images +└── templates/ # HTML templates +``` + +## Features + +### Authentication + +- User registration with email +- Login/logout with session management +- Password reset via email +- User profiles and settings + +### Billing & subscriptions + +- Pricing page with plan tiers +- Stripe Checkout integration +- Subscription management (upgrade/downgrade/cancel) +- Stripe webhook handling +- Demo mode when Stripe is not configured + +### Dashboard + +- Project CRUD (create, read, update, delete) +- Activity logging +- Usage metrics display +- Subscription status + +### Admin panel + +- Django admin at `/admin/` +- Plan management with subscriber counts +- Subscription management with status badges + +## Database models + +| Model | Description | +|-------|-------------| +| `User` | Custom user model with authentication and Stripe customer ID | +| `Plan` | Subscription plans with pricing and Stripe price IDs | +| `Subscription` | User subscriptions with status tracking | +| `Project` | User projects with activity logging | +| `ActivityLog` | Audit trail for user actions | + +## Key dependencies + +- **Django** - Web framework +- **Stripe** - Payment processing +- **Whitenoise** - Static file serving +- **dj-database-url** - Database configuration from URL +- **python-dotenv** - Environment variable management diff --git a/apps/error-tracking/no-posthog-django/django3-saas/accounts/__init__.py b/apps/error-tracking/no-posthog-django/django3-saas/accounts/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/error-tracking/no-posthog-django/django3-saas/accounts/admin.py b/apps/error-tracking/no-posthog-django/django3-saas/accounts/admin.py new file mode 100644 index 000000000..6b043c786 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/accounts/admin.py @@ -0,0 +1,11 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin +from .models import User + + +@admin.register(User) +class CustomUserAdmin(UserAdmin): + list_display = ['username', 'email', 'is_staff', 'date_joined'] + fieldsets = UserAdmin.fieldsets + ( + ('Profile', {'fields': ('bio',)}), + ) diff --git a/apps/error-tracking/no-posthog-django/django3-saas/accounts/forms.py b/apps/error-tracking/no-posthog-django/django3-saas/accounts/forms.py new file mode 100644 index 000000000..3edb36845 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/accounts/forms.py @@ -0,0 +1,22 @@ +from django import forms +from django.contrib.auth.forms import UserCreationForm, AuthenticationForm +from .models import User + + +class RegisterForm(UserCreationForm): + email = forms.EmailField(required=True) + company_name = forms.CharField(max_length=200, required=False) + + class Meta: + model = User + fields = ['username', 'email', 'company_name', 'password1', 'password2'] + + +class LoginForm(AuthenticationForm): + username = forms.CharField(widget=forms.TextInput(attrs={'autofocus': True})) + + +class ProfileForm(forms.ModelForm): + class Meta: + model = User + fields = ['first_name', 'last_name', 'email', 'company_name'] diff --git a/apps/error-tracking/no-posthog-django/django3-saas/accounts/migrations/0001_initial.py b/apps/error-tracking/no-posthog-django/django3-saas/accounts/migrations/0001_initial.py new file mode 100644 index 000000000..c4e96d1fa --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/accounts/migrations/0001_initial.py @@ -0,0 +1,43 @@ +from django.db import migrations, models +import django.contrib.auth.models +import django.contrib.auth.validators +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('company_name', models.CharField(blank=True, max_length=200)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/apps/error-tracking/no-posthog-django/django3-saas/accounts/migrations/0002_user_email_verified_at_user_stripe_customer_id.py b/apps/error-tracking/no-posthog-django/django3-saas/accounts/migrations/0002_user_email_verified_at_user_stripe_customer_id.py new file mode 100644 index 000000000..cc6746b56 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/accounts/migrations/0002_user_email_verified_at_user_stripe_customer_id.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.27 on 2026-01-21 22:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='email_verified_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='user', + name='stripe_customer_id', + field=models.CharField(blank=True, max_length=100), + ), + ] diff --git a/apps/error-tracking/no-posthog-django/django3-saas/accounts/migrations/__init__.py b/apps/error-tracking/no-posthog-django/django3-saas/accounts/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/error-tracking/no-posthog-django/django3-saas/accounts/models.py b/apps/error-tracking/no-posthog-django/django3-saas/accounts/models.py new file mode 100644 index 000000000..747a354d4 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/accounts/models.py @@ -0,0 +1,31 @@ +from hashlib import md5 +from django.contrib.auth.models import AbstractUser +from django.db import models + + +class User(AbstractUser): + company_name = models.CharField(max_length=200, blank=True) + email_verified_at = models.DateTimeField(null=True, blank=True) + + # Stripe customer ID for billing + stripe_customer_id = models.CharField(max_length=100, blank=True) + + def __str__(self): + return self.username + + def avatar_url(self, size=128): + digest = md5(self.email.lower().encode('utf-8')).hexdigest() + return f'https://www.gravatar.com/avatar/{digest}?d=identicon&s={size}' + + def get_active_subscription(self): + return self.subscriptions.filter(status='active').first() + + def is_subscribed(self): + return self.subscriptions.filter(status='active').exists() + + def get_plan(self): + sub = self.get_active_subscription() + return sub.plan if sub else None + + def is_email_verified(self): + return self.email_verified_at is not None diff --git a/apps/error-tracking/no-posthog-django/django3-saas/accounts/urls.py b/apps/error-tracking/no-posthog-django/django3-saas/accounts/urls.py new file mode 100644 index 000000000..52720159b --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/accounts/urls.py @@ -0,0 +1,17 @@ +from django.urls import path +from . import views + +app_name = 'accounts' + +urlpatterns = [ + path('login/', views.CustomLoginView.as_view(), name='login'), + path('logout/', views.CustomLogoutView.as_view(), name='logout'), + path('register/', views.register, name='register'), + path('settings/', views.settings, name='settings'), + + # Password reset + path('password-reset/', views.CustomPasswordResetView.as_view(), name='password_reset'), + path('password-reset/done/', views.CustomPasswordResetDoneView.as_view(), name='password_reset_done'), + path('password-reset///', views.CustomPasswordResetConfirmView.as_view(), name='password_reset_confirm'), + path('password-reset/complete/', views.CustomPasswordResetCompleteView.as_view(), name='password_reset_complete'), +] diff --git a/apps/error-tracking/no-posthog-django/django3-saas/accounts/views.py b/apps/error-tracking/no-posthog-django/django3-saas/accounts/views.py new file mode 100644 index 000000000..03b8ea066 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/accounts/views.py @@ -0,0 +1,71 @@ +from django.shortcuts import render, redirect +from django.contrib.auth import login +from django.contrib.auth.decorators import login_required +from django.contrib.auth.views import ( + LoginView, LogoutView, + PasswordResetView, PasswordResetDoneView, + PasswordResetConfirmView, PasswordResetCompleteView +) +from django.contrib import messages +from django.urls import reverse_lazy +from .forms import RegisterForm, LoginForm, ProfileForm + + +class CustomLoginView(LoginView): + form_class = LoginForm + template_name = 'accounts/login.html' + + +class CustomLogoutView(LogoutView): + next_page = reverse_lazy('accounts:login') + + +class CustomPasswordResetView(PasswordResetView): + template_name = 'accounts/password_reset.html' + email_template_name = 'accounts/password_reset_email.html' + subject_template_name = 'accounts/password_reset_subject.txt' + success_url = reverse_lazy('accounts:password_reset_done') + + +class CustomPasswordResetDoneView(PasswordResetDoneView): + template_name = 'accounts/password_reset_done.html' + + +class CustomPasswordResetConfirmView(PasswordResetConfirmView): + template_name = 'accounts/password_reset_confirm.html' + success_url = reverse_lazy('accounts:password_reset_complete') + + +class CustomPasswordResetCompleteView(PasswordResetCompleteView): + template_name = 'accounts/password_reset_complete.html' + + +def register(request): + if request.user.is_authenticated: + return redirect('dashboard:index') + + if request.method == 'POST': + form = RegisterForm(request.POST) + if form.is_valid(): + user = form.save() + login(request, user) + messages.success(request, 'Registration successful. Welcome!') + return redirect('dashboard:index') + else: + form = RegisterForm() + + return render(request, 'accounts/register.html', {'form': form}) + + +@login_required +def settings(request): + if request.method == 'POST': + form = ProfileForm(request.POST, instance=request.user) + if form.is_valid(): + form.save() + messages.success(request, 'Settings updated.') + return redirect('accounts:settings') + else: + form = ProfileForm(instance=request.user) + + return render(request, 'accounts/settings.html', {'form': form}) diff --git a/apps/error-tracking/no-posthog-django/django3-saas/billing/__init__.py b/apps/error-tracking/no-posthog-django/django3-saas/billing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/error-tracking/no-posthog-django/django3-saas/billing/admin.py b/apps/error-tracking/no-posthog-django/django3-saas/billing/admin.py new file mode 100644 index 000000000..489b3f002 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/billing/admin.py @@ -0,0 +1,117 @@ +from django.contrib import admin +from django.utils.html import format_html +from django.db.models import Count, Sum +from .models import Plan, Subscription + + +@admin.register(Plan) +class PlanAdmin(admin.ModelAdmin): + list_display = ['name', 'price_display', 'interval', 'subscriber_count', 'is_active', 'is_featured', 'sort_order'] + list_filter = ['is_active', 'is_featured', 'interval'] + prepopulated_fields = {'slug': ('name',)} + ordering = ['sort_order', 'price'] + search_fields = ['name', 'slug'] + readonly_fields = ['created_at', 'updated_at'] + + fieldsets = ( + (None, { + 'fields': ('name', 'slug', 'description') + }), + ('Pricing', { + 'fields': ('price', 'currency', 'interval') + }), + ('Features', { + 'fields': ('features',), + 'description': 'Enter features as a JSON array: ["Feature 1", "Feature 2"]' + }), + ('Display', { + 'fields': ('is_active', 'is_featured', 'sort_order') + }), + ('Stripe', { + 'fields': ('stripe_price_id',), + 'classes': ('collapse',) + }), + ('Timestamps', { + 'fields': ('created_at', 'updated_at'), + 'classes': ('collapse',) + }), + ) + + def price_display(self, obj): + return obj.get_price_display() + price_display.short_description = 'Price' + + def subscriber_count(self, obj): + count = obj.subscriptions.filter(status='active').count() + return format_html('{}', count) + subscriber_count.short_description = 'Active Subscribers' + + def get_queryset(self, request): + return super().get_queryset(request).annotate( + active_subs=Count('subscriptions', filter=models.Q(subscriptions__status='active')) + ) + + +@admin.register(Subscription) +class SubscriptionAdmin(admin.ModelAdmin): + list_display = ['user', 'plan', 'status_badge', 'current_period_start', 'current_period_end', 'is_demo'] + list_filter = ['status', 'plan', 'created_at'] + search_fields = ['user__username', 'user__email', 'stripe_subscription_id'] + raw_id_fields = ['user'] + readonly_fields = ['created_at', 'updated_at'] + date_hierarchy = 'created_at' + + fieldsets = ( + (None, { + 'fields': ('user', 'plan', 'status') + }), + ('Billing Period', { + 'fields': ('current_period_start', 'current_period_end', 'canceled_at') + }), + ('Stripe', { + 'fields': ('stripe_subscription_id', 'stripe_customer_id'), + 'classes': ('collapse',) + }), + ('Timestamps', { + 'fields': ('created_at', 'updated_at'), + 'classes': ('collapse',) + }), + ) + + def status_badge(self, obj): + colors = { + 'active': '#28a745', + 'canceled': '#dc3545', + 'past_due': '#ffc107', + 'trialing': '#17a2b8', + 'paused': '#6c757d', + } + color = colors.get(obj.status, '#6c757d') + return format_html( + '{}', + color, obj.get_status_display() + ) + status_badge.short_description = 'Status' + + def is_demo(self, obj): + if obj.stripe_subscription_id and obj.stripe_subscription_id.startswith('sub_demo_'): + return format_html('Demo') + return format_html('Live') + is_demo.short_description = 'Mode' + + actions = ['mark_as_canceled', 'mark_as_active'] + + def mark_as_canceled(self, request, queryset): + from django.utils import timezone + updated = queryset.update(status='canceled', canceled_at=timezone.now()) + self.message_user(request, f'{updated} subscription(s) marked as canceled.') + mark_as_canceled.short_description = 'Mark selected as canceled' + + def mark_as_active(self, request, queryset): + updated = queryset.update(status='active', canceled_at=None) + self.message_user(request, f'{updated} subscription(s) marked as active.') + mark_as_active.short_description = 'Mark selected as active' + + +# Import models for use in admin queryset +from django.db import models diff --git a/apps/error-tracking/no-posthog-django/django3-saas/billing/management/__init__.py b/apps/error-tracking/no-posthog-django/django3-saas/billing/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/error-tracking/no-posthog-django/django3-saas/billing/management/commands/__init__.py b/apps/error-tracking/no-posthog-django/django3-saas/billing/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/error-tracking/no-posthog-django/django3-saas/billing/management/commands/seed_plans.py b/apps/error-tracking/no-posthog-django/django3-saas/billing/management/commands/seed_plans.py new file mode 100644 index 000000000..425ff16be --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/billing/management/commands/seed_plans.py @@ -0,0 +1,75 @@ +from django.core.management.base import BaseCommand +from billing.models import Plan + + +class Command(BaseCommand): + help = 'Seed the database with demo pricing plans' + + def handle(self, *args, **options): + plans_data = [ + { + 'name': 'Starter', + 'slug': 'starter', + 'description': 'Perfect for individuals and small projects.', + 'price': 0, + 'interval': 'month', + 'features': [ + '1 project', + '1,000 API requests/month', + 'Community support', + 'Basic analytics', + ], + 'is_active': True, + 'is_featured': False, + 'sort_order': 1, + 'stripe_price_id': '', + }, + { + 'name': 'Growth', + 'slug': 'growth', + 'description': 'For growing teams that need more power.', + 'price': 29, + 'interval': 'month', + 'features': [ + '10 projects', + '50,000 API requests/month', + 'Email support', + 'Advanced analytics', + 'Team collaboration', + ], + 'is_active': True, + 'is_featured': True, + 'sort_order': 2, + 'stripe_price_id': 'price_growth_monthly', + }, + { + 'name': 'Scale', + 'slug': 'scale', + 'description': 'For businesses that need unlimited scale.', + 'price': 99, + 'interval': 'month', + 'features': [ + 'Unlimited projects', + 'Unlimited API requests', + 'Priority support', + 'Custom analytics', + 'Team collaboration', + 'SSO integration', + 'Dedicated account manager', + ], + 'is_active': True, + 'is_featured': False, + 'sort_order': 3, + 'stripe_price_id': 'price_scale_monthly', + }, + ] + + for plan_data in plans_data: + plan, created = Plan.objects.update_or_create( + slug=plan_data['slug'], + defaults=plan_data + ) + status = 'Created' if created else 'Updated' + self.stdout.write(f'{status}: {plan.name}') + + self.stdout.write(self.style.SUCCESS('Successfully seeded plans')) diff --git a/apps/error-tracking/no-posthog-django/django3-saas/billing/migrations/0001_initial.py b/apps/error-tracking/no-posthog-django/django3-saas/billing/migrations/0001_initial.py new file mode 100644 index 000000000..a7b81374e --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/billing/migrations/0001_initial.py @@ -0,0 +1,56 @@ +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Plan', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('slug', models.SlugField(unique=True)), + ('description', models.TextField(blank=True)), + ('price', models.DecimalField(decimal_places=2, max_digits=10)), + ('currency', models.CharField(default='USD', max_length=3)), + ('interval', models.CharField(choices=[('month', 'Monthly'), ('year', 'Yearly')], default='month', max_length=20)), + ('features', models.JSONField(default=list)), + ('is_active', models.BooleanField(default=True)), + ('is_featured', models.BooleanField(default=False)), + ('sort_order', models.IntegerField(default=0)), + ('stripe_price_id', models.CharField(blank=True, max_length=100)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'ordering': ['sort_order', 'price'], + }, + ), + migrations.CreateModel( + name='Subscription', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('active', 'Active'), ('canceled', 'Canceled'), ('past_due', 'Past Due'), ('trialing', 'Trialing'), ('paused', 'Paused')], default='active', max_length=20)), + ('current_period_start', models.DateTimeField()), + ('current_period_end', models.DateTimeField()), + ('canceled_at', models.DateTimeField(blank=True, null=True)), + ('stripe_subscription_id', models.CharField(blank=True, max_length=100)), + ('stripe_customer_id', models.CharField(blank=True, max_length=100)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('plan', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='subscriptions', to='billing.plan')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subscriptions', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/apps/error-tracking/no-posthog-django/django3-saas/billing/migrations/__init__.py b/apps/error-tracking/no-posthog-django/django3-saas/billing/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/error-tracking/no-posthog-django/django3-saas/billing/models.py b/apps/error-tracking/no-posthog-django/django3-saas/billing/models.py new file mode 100644 index 000000000..282e2207f --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/billing/models.py @@ -0,0 +1,71 @@ +from django.db import models +from django.conf import settings + + +class Plan(models.Model): + name = models.CharField(max_length=100) + slug = models.SlugField(unique=True) + description = models.TextField(blank=True) + price = models.DecimalField(max_digits=10, decimal_places=2) + currency = models.CharField(max_length=3, default='USD') + interval = models.CharField( + max_length=20, + choices=[('month', 'Monthly'), ('year', 'Yearly')], + default='month' + ) + features = models.JSONField(default=list) + is_active = models.BooleanField(default=True) + is_featured = models.BooleanField(default=False) + sort_order = models.IntegerField(default=0) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # Stripe integration (optional) + stripe_price_id = models.CharField(max_length=100, blank=True) + + class Meta: + ordering = ['sort_order', 'price'] + + def __str__(self): + return self.name + + def get_price_display(self): + if self.price == 0: + return 'Free' + return f'${self.price}/{self.interval}' + + +class Subscription(models.Model): + STATUS_CHOICES = [ + ('active', 'Active'), + ('canceled', 'Canceled'), + ('past_due', 'Past Due'), + ('trialing', 'Trialing'), + ('paused', 'Paused'), + ] + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name='subscriptions' + ) + plan = models.ForeignKey(Plan, on_delete=models.PROTECT, related_name='subscriptions') + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active') + current_period_start = models.DateTimeField() + current_period_end = models.DateTimeField() + canceled_at = models.DateTimeField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # Stripe integration (optional) + stripe_subscription_id = models.CharField(max_length=100, blank=True) + stripe_customer_id = models.CharField(max_length=100, blank=True) + + class Meta: + ordering = ['-created_at'] + + def __str__(self): + return f'{self.user.username} - {self.plan.name}' + + def is_active(self): + return self.status == 'active' diff --git a/apps/error-tracking/no-posthog-django/django3-saas/billing/urls.py b/apps/error-tracking/no-posthog-django/django3-saas/billing/urls.py new file mode 100644 index 000000000..6aa1342da --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/billing/urls.py @@ -0,0 +1,15 @@ +from django.urls import path +from . import views + +app_name = 'billing' + +urlpatterns = [ + path('pricing/', views.pricing, name='pricing'), + path('subscribe//', views.subscribe, name='subscribe'), + path('success/', views.success, name='success'), + path('manage/', views.manage, name='manage'), + path('change-plan//', views.change_plan, name='change_plan'), + path('cancel/', views.cancel, name='cancel'), + path('portal/', views.billing_portal, name='billing_portal'), + path('webhook/', views.webhook, name='webhook'), +] diff --git a/apps/error-tracking/no-posthog-django/django3-saas/billing/views.py b/apps/error-tracking/no-posthog-django/django3-saas/billing/views.py new file mode 100644 index 000000000..075923e23 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/billing/views.py @@ -0,0 +1,327 @@ +import uuid +from django.shortcuts import render, redirect, get_object_or_404 +from django.contrib.auth.decorators import login_required +from django.contrib import messages +from django.conf import settings +from django.http import HttpResponse +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_POST +from django.utils import timezone +from datetime import timedelta +from .models import Plan, Subscription + +# Check if Stripe is configured +STRIPE_CONFIGURED = bool(getattr(settings, 'STRIPE_SECRET_KEY', '')) + +if STRIPE_CONFIGURED: + import stripe + stripe.api_key = settings.STRIPE_SECRET_KEY + + +def pricing(request): + """Display pricing plans.""" + plans = Plan.objects.filter(is_active=True) + return render(request, 'billing/pricing.html', {'plans': plans}) + + +@login_required +def subscribe(request, plan_slug): + """Subscribe to a plan - redirects to Stripe Checkout or creates demo subscription.""" + plan = get_object_or_404(Plan, slug=plan_slug, is_active=True) + + # Check if user already has an active subscription + existing = request.user.subscriptions.filter(status='active').first() + if existing: + messages.warning(request, 'You already have an active subscription.') + return redirect('billing:manage') + + if request.method == 'POST': + if STRIPE_CONFIGURED and plan.stripe_price_id: + # Create Stripe Checkout Session + try: + checkout_session = stripe.checkout.Session.create( + customer_email=request.user.email, + payment_method_types=['card'], + line_items=[{ + 'price': plan.stripe_price_id, + 'quantity': 1, + }], + mode='subscription', + success_url=request.build_absolute_uri('/billing/success/') + '?session_id={CHECKOUT_SESSION_ID}', + cancel_url=request.build_absolute_uri('/billing/pricing/'), + metadata={ + 'user_id': request.user.id, + 'plan_id': plan.id, + }, + allow_promotion_codes=True, + ) + return redirect(checkout_session.url) + except Exception as e: + messages.error(request, f'Payment error: {str(e)}') + return redirect('billing:pricing') + else: + # Demo mode - create subscription directly + now = timezone.now() + Subscription.objects.create( + user=request.user, + plan=plan, + status='active', + current_period_start=now, + current_period_end=now + timedelta(days=30 if plan.interval == 'month' else 365), + stripe_subscription_id=f'sub_demo_{uuid.uuid4().hex[:12]}', + ) + messages.success(request, f'Successfully subscribed to {plan.name}! (Demo mode)') + return redirect('dashboard:index') + + return render(request, 'billing/subscribe.html', { + 'plan': plan, + 'stripe_configured': STRIPE_CONFIGURED, + }) + + +@login_required +def success(request): + """Handle successful Stripe checkout.""" + session_id = request.GET.get('session_id') + + if STRIPE_CONFIGURED and session_id: + try: + session = stripe.checkout.Session.retrieve(session_id) + messages.success(request, 'Subscription successful! Welcome aboard.') + except Exception: + messages.warning(request, 'Could not verify payment. Please check your subscription status.') + + return redirect('dashboard:index') + + +@login_required +def manage(request): + """Manage subscription - view current plan, upgrade/downgrade options.""" + subscription = request.user.get_active_subscription() + plans = Plan.objects.filter(is_active=True) + return render(request, 'billing/manage.html', { + 'subscription': subscription, + 'plans': plans, + 'stripe_configured': STRIPE_CONFIGURED, + }) + + +@login_required +def change_plan(request, plan_slug): + """Change subscription plan.""" + plan = get_object_or_404(Plan, slug=plan_slug, is_active=True) + subscription = request.user.get_active_subscription() + + if not subscription: + return redirect('billing:subscribe', plan_slug=plan_slug) + + if request.method == 'POST': + if STRIPE_CONFIGURED and subscription.stripe_subscription_id and not subscription.stripe_subscription_id.startswith('sub_demo_'): + # Update Stripe subscription + try: + stripe_sub = stripe.Subscription.retrieve(subscription.stripe_subscription_id) + stripe.Subscription.modify( + subscription.stripe_subscription_id, + items=[{ + 'id': stripe_sub['items']['data'][0].id, + 'price': plan.stripe_price_id, + }], + proration_behavior='create_prorations', + ) + subscription.plan = plan + subscription.save() + messages.success(request, f'Plan changed to {plan.name}.') + except Exception as e: + messages.error(request, f'Error changing plan: {str(e)}') + else: + # Demo mode + subscription.plan = plan + subscription.save() + messages.success(request, f'Plan changed to {plan.name}. (Demo mode)') + + return redirect('billing:manage') + + return render(request, 'billing/change_plan.html', { + 'plan': plan, + 'subscription': subscription, + }) + + +@login_required +def cancel(request): + """Cancel subscription.""" + subscription = request.user.get_active_subscription() + + if not subscription: + messages.warning(request, 'No active subscription to cancel.') + return redirect('billing:manage') + + if request.method == 'POST': + if STRIPE_CONFIGURED and subscription.stripe_subscription_id and not subscription.stripe_subscription_id.startswith('sub_demo_'): + # Cancel at period end in Stripe + try: + stripe.Subscription.modify( + subscription.stripe_subscription_id, + cancel_at_period_end=True, + ) + except Exception as e: + messages.error(request, f'Error canceling: {str(e)}') + return redirect('billing:manage') + + subscription.status = 'canceled' + subscription.canceled_at = timezone.now() + subscription.save() + messages.success(request, 'Subscription canceled. You will have access until the end of your billing period.') + return redirect('billing:manage') + + return render(request, 'billing/cancel.html', {'subscription': subscription}) + + +@login_required +def billing_portal(request): + """Redirect to Stripe Billing Portal for self-service management.""" + if not STRIPE_CONFIGURED: + messages.info(request, 'Billing portal is not available in demo mode.') + return redirect('billing:manage') + + subscription = request.user.get_active_subscription() + if not subscription or not subscription.stripe_customer_id: + messages.warning(request, 'No billing information found.') + return redirect('billing:manage') + + try: + portal_session = stripe.billing_portal.Session.create( + customer=subscription.stripe_customer_id, + return_url=request.build_absolute_uri('/billing/manage/'), + ) + return redirect(portal_session.url) + except Exception as e: + messages.error(request, f'Error accessing billing portal: {str(e)}') + return redirect('billing:manage') + + +@csrf_exempt +@require_POST +def webhook(request): + """Handle Stripe webhooks.""" + payload = request.body + sig_header = request.META.get('HTTP_STRIPE_SIGNATURE') + webhook_secret = getattr(settings, 'STRIPE_WEBHOOK_SECRET', '') + + if not STRIPE_CONFIGURED or not webhook_secret: + return HttpResponse(status=400) + + try: + event = stripe.Webhook.construct_event( + payload, sig_header, webhook_secret + ) + except ValueError: + return HttpResponse(status=400) + except Exception: + return HttpResponse(status=400) + + # Handle the event + if event['type'] == 'checkout.session.completed': + session = event['data']['object'] + _handle_checkout_completed(session) + elif event['type'] == 'customer.subscription.updated': + subscription_data = event['data']['object'] + _handle_subscription_updated(subscription_data) + elif event['type'] == 'customer.subscription.deleted': + subscription_data = event['data']['object'] + _handle_subscription_deleted(subscription_data) + elif event['type'] == 'invoice.payment_failed': + invoice = event['data']['object'] + _handle_payment_failed(invoice) + + return HttpResponse(status=200) + + +def _handle_checkout_completed(session): + """Create subscription after successful checkout.""" + from accounts.models import User + + user_id = session.get('metadata', {}).get('user_id') + plan_id = session.get('metadata', {}).get('plan_id') + + if not user_id or not plan_id: + return + + try: + user = User.objects.get(id=user_id) + plan = Plan.objects.get(id=plan_id) + except (User.DoesNotExist, Plan.DoesNotExist): + return + + # Get subscription details from Stripe + stripe_sub = stripe.Subscription.retrieve(session['subscription']) + + Subscription.objects.create( + user=user, + plan=plan, + status='active', + current_period_start=timezone.datetime.fromtimestamp( + stripe_sub['current_period_start'], tz=timezone.utc + ), + current_period_end=timezone.datetime.fromtimestamp( + stripe_sub['current_period_end'], tz=timezone.utc + ), + stripe_subscription_id=stripe_sub['id'], + stripe_customer_id=stripe_sub['customer'], + ) + + +def _handle_subscription_updated(subscription_data): + """Update subscription status.""" + try: + subscription = Subscription.objects.get( + stripe_subscription_id=subscription_data['id'] + ) + except Subscription.DoesNotExist: + return + + status_map = { + 'active': 'active', + 'past_due': 'past_due', + 'canceled': 'canceled', + 'trialing': 'trialing', + 'paused': 'paused', + } + + subscription.status = status_map.get(subscription_data['status'], 'active') + subscription.current_period_start = timezone.datetime.fromtimestamp( + subscription_data['current_period_start'], tz=timezone.utc + ) + subscription.current_period_end = timezone.datetime.fromtimestamp( + subscription_data['current_period_end'], tz=timezone.utc + ) + subscription.save() + + +def _handle_subscription_deleted(subscription_data): + """Handle subscription cancellation.""" + try: + subscription = Subscription.objects.get( + stripe_subscription_id=subscription_data['id'] + ) + subscription.status = 'canceled' + subscription.canceled_at = timezone.now() + subscription.save() + except Subscription.DoesNotExist: + pass + + +def _handle_payment_failed(invoice): + """Handle failed payment.""" + subscription_id = invoice.get('subscription') + if not subscription_id: + return + + try: + subscription = Subscription.objects.get( + stripe_subscription_id=subscription_id + ) + subscription.status = 'past_due' + subscription.save() + except Subscription.DoesNotExist: + pass diff --git a/apps/error-tracking/no-posthog-django/django3-saas/config/__init__.py b/apps/error-tracking/no-posthog-django/django3-saas/config/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/error-tracking/no-posthog-django/django3-saas/config/settings.py b/apps/error-tracking/no-posthog-django/django3-saas/config/settings.py new file mode 100644 index 000000000..3c02dea1b --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/config/settings.py @@ -0,0 +1,113 @@ +import os +from pathlib import Path +from dotenv import load_dotenv + +load_dotenv() + +BASE_DIR = Path(__file__).resolve().parent.parent + +SECRET_KEY = os.environ.get('SECRET_KEY', 'django-insecure-dev-key-change-in-production') + +DEBUG = os.environ.get('DEBUG', 'True').lower() in ('true', '1', 'yes') + +ALLOWED_HOSTS = [h.strip() for h in os.environ.get('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',')] + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'accounts', + 'billing', + 'dashboard', + 'marketing', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'config.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'config.wsgi.application' + +DATABASE_URL = os.environ.get('DATABASE_URL', '') +if DATABASE_URL: + import dj_database_url + DATABASES = { + 'default': dj_database_url.parse(DATABASE_URL) + } +else: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } + } + +AUTH_PASSWORD_VALIDATORS = [ + {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, + {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'}, + {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, + {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, +] + +AUTH_USER_MODEL = 'accounts.User' + +LANGUAGE_CODE = 'en-us' +TIME_ZONE = 'UTC' +USE_I18N = True +USE_TZ = True + +STATIC_URL = '/static/' +STATIC_ROOT = BASE_DIR / 'staticfiles' +STATICFILES_DIRS = [BASE_DIR / 'static'] +STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +LOGIN_URL = 'accounts:login' +LOGIN_REDIRECT_URL = 'dashboard:index' +LOGOUT_REDIRECT_URL = 'marketing:home' + +# Email settings (console backend for development) +EMAIL_BACKEND = os.environ.get( + 'EMAIL_BACKEND', + 'django.core.mail.backends.console.EmailBackend' +) +EMAIL_HOST = os.environ.get('EMAIL_HOST', 'localhost') +EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 25)) +EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER', '') +EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD', '') +EMAIL_USE_TLS = os.environ.get('EMAIL_USE_TLS', 'False').lower() in ('true', '1', 'yes') +DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'noreply@example.com') + +# Stripe settings (optional - for billing integration) +STRIPE_PUBLIC_KEY = os.environ.get('STRIPE_PUBLIC_KEY', '') +STRIPE_SECRET_KEY = os.environ.get('STRIPE_SECRET_KEY', '') +STRIPE_WEBHOOK_SECRET = os.environ.get('STRIPE_WEBHOOK_SECRET', '') diff --git a/apps/error-tracking/no-posthog-django/django3-saas/config/urls.py b/apps/error-tracking/no-posthog-django/django3-saas/config/urls.py new file mode 100644 index 000000000..a3d56f6d2 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/config/urls.py @@ -0,0 +1,13 @@ +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path('admin/', admin.site.urls), + path('accounts/', include('accounts.urls')), + path('billing/', include('billing.urls')), + path('dashboard/', include('dashboard.urls')), + path('', include('marketing.urls')), +] + +handler404 = 'config.views.handler404' +handler500 = 'config.views.handler500' diff --git a/apps/error-tracking/no-posthog-django/django3-saas/config/views.py b/apps/error-tracking/no-posthog-django/django3-saas/config/views.py new file mode 100644 index 000000000..43a79d154 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/config/views.py @@ -0,0 +1,9 @@ +from django.shortcuts import render + + +def handler404(request, exception): + return render(request, 'errors/404.html', status=404) + + +def handler500(request): + return render(request, 'errors/500.html', status=500) diff --git a/apps/error-tracking/no-posthog-django/django3-saas/config/wsgi.py b/apps/error-tracking/no-posthog-django/django3-saas/config/wsgi.py new file mode 100644 index 000000000..885c6e5b7 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/config/wsgi.py @@ -0,0 +1,5 @@ +import os +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +application = get_wsgi_application() diff --git a/apps/error-tracking/no-posthog-django/django3-saas/dashboard/__init__.py b/apps/error-tracking/no-posthog-django/django3-saas/dashboard/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/error-tracking/no-posthog-django/django3-saas/dashboard/admin.py b/apps/error-tracking/no-posthog-django/django3-saas/dashboard/admin.py new file mode 100644 index 000000000..abd0a00c9 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/dashboard/admin.py @@ -0,0 +1,18 @@ +from django.contrib import admin +from .models import Project, ActivityLog + + +@admin.register(Project) +class ProjectAdmin(admin.ModelAdmin): + list_display = ['name', 'owner', 'is_active', 'created_at'] + list_filter = ['is_active', 'created_at'] + search_fields = ['name', 'description', 'owner__username'] + raw_id_fields = ['owner'] + + +@admin.register(ActivityLog) +class ActivityLogAdmin(admin.ModelAdmin): + list_display = ['user', 'action', 'created_at'] + list_filter = ['action', 'created_at'] + search_fields = ['user__username', 'action', 'description'] + raw_id_fields = ['user'] diff --git a/apps/error-tracking/no-posthog-django/django3-saas/dashboard/forms.py b/apps/error-tracking/no-posthog-django/django3-saas/dashboard/forms.py new file mode 100644 index 000000000..547886cb7 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/dashboard/forms.py @@ -0,0 +1,11 @@ +from django import forms +from .models import Project + + +class ProjectForm(forms.ModelForm): + class Meta: + model = Project + fields = ['name', 'description', 'is_active'] + widgets = { + 'description': forms.Textarea(attrs={'rows': 4}), + } diff --git a/apps/error-tracking/no-posthog-django/django3-saas/dashboard/migrations/0001_initial.py b/apps/error-tracking/no-posthog-django/django3-saas/dashboard/migrations/0001_initial.py new file mode 100644 index 000000000..606d09e60 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/dashboard/migrations/0001_initial.py @@ -0,0 +1,43 @@ +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Project', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ('description', models.TextField(blank=True)), + ('is_active', models.BooleanField(default=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='projects', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='ActivityLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('action', models.CharField(max_length=100)), + ('description', models.TextField(blank=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activities', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/apps/error-tracking/no-posthog-django/django3-saas/dashboard/migrations/__init__.py b/apps/error-tracking/no-posthog-django/django3-saas/dashboard/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/error-tracking/no-posthog-django/django3-saas/dashboard/models.py b/apps/error-tracking/no-posthog-django/django3-saas/dashboard/models.py new file mode 100644 index 000000000..ce9aeed42 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/dashboard/models.py @@ -0,0 +1,38 @@ +from django.db import models +from django.conf import settings + + +class Project(models.Model): + owner = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name='projects' + ) + name = models.CharField(max_length=200) + description = models.TextField(blank=True) + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ['-created_at'] + + def __str__(self): + return self.name + + +class ActivityLog(models.Model): + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name='activities' + ) + action = models.CharField(max_length=100) + description = models.TextField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['-created_at'] + + def __str__(self): + return f'{self.user.username} - {self.action}' diff --git a/apps/error-tracking/no-posthog-django/django3-saas/dashboard/urls.py b/apps/error-tracking/no-posthog-django/django3-saas/dashboard/urls.py new file mode 100644 index 000000000..fd79a18f8 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/dashboard/urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from . import views + +app_name = 'dashboard' + +urlpatterns = [ + path('', views.index, name='index'), + path('projects/', views.projects, name='projects'), + path('projects/create/', views.create_project, name='create_project'), + path('projects//edit/', views.edit_project, name='edit_project'), + path('projects//delete/', views.delete_project, name='delete_project'), +] diff --git a/apps/error-tracking/no-posthog-django/django3-saas/dashboard/views.py b/apps/error-tracking/no-posthog-django/django3-saas/dashboard/views.py new file mode 100644 index 000000000..be99138b9 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/dashboard/views.py @@ -0,0 +1,111 @@ +from django.shortcuts import render, redirect, get_object_or_404 +from django.contrib.auth.decorators import login_required +from django.contrib import messages +from django.utils import timezone +from datetime import timedelta +from .models import Project, ActivityLog +from .forms import ProjectForm + + +@login_required +def index(request): + projects = request.user.projects.all()[:5] + activities = request.user.activities.all()[:10] + subscription = request.user.get_active_subscription() + + # Calculate usage metrics for the user + now = timezone.now() + thirty_days_ago = now - timedelta(days=30) + + metrics = { + 'project_count': request.user.projects.count(), + 'active_projects': request.user.projects.filter(is_active=True).count(), + 'activities_this_month': request.user.activities.filter( + created_at__gte=thirty_days_ago + ).count(), + } + + # Add subscription info + if subscription: + days_remaining = (subscription.current_period_end - now).days + metrics['days_remaining'] = max(0, days_remaining) + metrics['plan_name'] = subscription.plan.name + + return render(request, 'dashboard/index.html', { + 'projects': projects, + 'activities': activities, + 'subscription': subscription, + 'metrics': metrics, + }) + + +@login_required +def projects(request): + projects = request.user.projects.all() + return render(request, 'dashboard/projects.html', {'projects': projects}) + + +@login_required +def create_project(request): + if request.method == 'POST': + form = ProjectForm(request.POST) + if form.is_valid(): + project = form.save(commit=False) + project.owner = request.user + project.save() + + ActivityLog.objects.create( + user=request.user, + action='project_created', + description=f'Created project: {project.name}' + ) + + messages.success(request, 'Project created.') + return redirect('dashboard:projects') + else: + form = ProjectForm() + + return render(request, 'dashboard/create_project.html', {'form': form}) + + +@login_required +def edit_project(request, pk): + project = get_object_or_404(Project, pk=pk, owner=request.user) + + if request.method == 'POST': + form = ProjectForm(request.POST, instance=project) + if form.is_valid(): + form.save() + + ActivityLog.objects.create( + user=request.user, + action='project_updated', + description=f'Updated project: {project.name}' + ) + + messages.success(request, 'Project updated.') + return redirect('dashboard:projects') + else: + form = ProjectForm(instance=project) + + return render(request, 'dashboard/edit_project.html', {'form': form, 'project': project}) + + +@login_required +def delete_project(request, pk): + project = get_object_or_404(Project, pk=pk, owner=request.user) + + if request.method == 'POST': + name = project.name + project.delete() + + ActivityLog.objects.create( + user=request.user, + action='project_deleted', + description=f'Deleted project: {name}' + ) + + messages.success(request, 'Project deleted.') + return redirect('dashboard:projects') + + return render(request, 'dashboard/delete_project.html', {'project': project}) diff --git a/apps/error-tracking/no-posthog-django/django3-saas/manage.py b/apps/error-tracking/no-posthog-django/django3-saas/manage.py new file mode 100644 index 000000000..fcc53d76a --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/manage.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +import os +import sys + +def main(): + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + +if __name__ == '__main__': + main() diff --git a/apps/error-tracking/no-posthog-django/django3-saas/marketing/__init__.py b/apps/error-tracking/no-posthog-django/django3-saas/marketing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/error-tracking/no-posthog-django/django3-saas/marketing/urls.py b/apps/error-tracking/no-posthog-django/django3-saas/marketing/urls.py new file mode 100644 index 000000000..5425d47ba --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/marketing/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from . import views + +app_name = 'marketing' + +urlpatterns = [ + path('', views.home, name='home'), + path('features/', views.features, name='features'), +] diff --git a/apps/error-tracking/no-posthog-django/django3-saas/marketing/views.py b/apps/error-tracking/no-posthog-django/django3-saas/marketing/views.py new file mode 100644 index 000000000..eebb4b294 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/marketing/views.py @@ -0,0 +1,11 @@ +from django.shortcuts import render + + +def home(request): + """Landing page for the SaaS application.""" + return render(request, 'marketing/home.html') + + +def features(request): + """Features page showcasing product capabilities.""" + return render(request, 'marketing/features.html') diff --git a/apps/error-tracking/no-posthog-django/django3-saas/requirements.txt b/apps/error-tracking/no-posthog-django/django3-saas/requirements.txt new file mode 100644 index 000000000..e10e2f8dc --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/requirements.txt @@ -0,0 +1,6 @@ +Django>=3.2,<5.0 +python-dotenv>=1.0.0 +gunicorn>=21.0.0 +whitenoise>=6.6.0 +dj-database-url>=2.0.0 +stripe>=7.0.0 diff --git a/apps/error-tracking/no-posthog-django/django3-saas/static/.gitkeep b/apps/error-tracking/no-posthog-django/django3-saas/static/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/login.html b/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/login.html new file mode 100644 index 000000000..9c6880b7d --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/login.html @@ -0,0 +1,18 @@ +{% extends 'base.html' %} + +{% block title %}Login{% endblock %} + +{% block content %} +

Login

+ +
+ {% csrf_token %} + {{ form.as_p }} + +
+ +

+ Forgot your password? +

+

Don't have an account? Register

+{% endblock %} diff --git a/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/password_reset.html b/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/password_reset.html new file mode 100644 index 000000000..4170b3d9a --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/password_reset.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} + +{% block title %}Reset Password{% endblock %} + +{% block content %} +

Reset Password

+

Enter your email address and we'll send you a link to reset your password.

+ +
+ {% csrf_token %} + {{ form.as_p }} + +
+ +

Back to Login

+{% endblock %} diff --git a/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/password_reset_complete.html b/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/password_reset_complete.html new file mode 100644 index 000000000..aa5a58577 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/password_reset_complete.html @@ -0,0 +1,9 @@ +{% extends 'base.html' %} + +{% block title %}Password Reset Complete{% endblock %} + +{% block content %} +

Password Reset Complete

+

Your password has been reset successfully.

+

Login Now

+{% endblock %} diff --git a/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/password_reset_confirm.html b/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/password_reset_confirm.html new file mode 100644 index 000000000..fb0d2a4c6 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/password_reset_confirm.html @@ -0,0 +1,18 @@ +{% extends 'base.html' %} + +{% block title %}Set New Password{% endblock %} + +{% block content %} +

Set New Password

+ +{% if validlink %} +
+ {% csrf_token %} + {{ form.as_p }} + +
+{% else %} +

This password reset link is invalid or has expired.

+

Request a new link

+{% endif %} +{% endblock %} diff --git a/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/password_reset_done.html b/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/password_reset_done.html new file mode 100644 index 000000000..832749f1a --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/password_reset_done.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} + +{% block title %}Password Reset Sent{% endblock %} + +{% block content %} +

Check Your Email

+

We've sent you an email with instructions to reset your password.

+

If you don't receive an email, make sure you entered the address you registered with.

+ +

Back to Login

+{% endblock %} diff --git a/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/password_reset_email.html b/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/password_reset_email.html new file mode 100644 index 000000000..ced6109aa --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/password_reset_email.html @@ -0,0 +1,10 @@ +You're receiving this email because you requested a password reset for your account. + +Please click the link below to reset your password: + +{{ protocol }}://{{ domain }}{% url 'accounts:password_reset_confirm' uidb64=uid token=token %} + +If you didn't request this, you can ignore this email. + +Thanks, +The Django SaaS Team diff --git a/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/password_reset_subject.txt b/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/password_reset_subject.txt new file mode 100644 index 000000000..7a3a81292 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/password_reset_subject.txt @@ -0,0 +1 @@ +Password Reset - Django SaaS \ No newline at end of file diff --git a/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/register.html b/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/register.html new file mode 100644 index 000000000..e6b09da53 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/register.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} + +{% block title %}Register{% endblock %} + +{% block content %} +

Register

+ +
+ {% csrf_token %} + {{ form.as_p }} + +
+ +

Already have an account? Login

+{% endblock %} diff --git a/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/settings.html b/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/settings.html new file mode 100644 index 000000000..14edba6ce --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/templates/accounts/settings.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} + +{% block title %}Settings{% endblock %} + +{% block content %} +

Settings

+ +
+

Profile Information

+
+ {% csrf_token %} + {{ form.as_p }} + +
+
+ +
+

Security

+

+ Change Password +

+
+{% endblock %} diff --git a/apps/error-tracking/no-posthog-django/django3-saas/templates/base.html b/apps/error-tracking/no-posthog-django/django3-saas/templates/base.html new file mode 100644 index 000000000..6da2962b1 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/templates/base.html @@ -0,0 +1,87 @@ + + + + + + {% block title %}Django SaaS{% endblock %} + + + {% block extra_css %}{% endblock %} + + +
+ +
+ +
+ {% if messages %} + {% for message in messages %} +

{{ message }}

+ {% endfor %} + {% endif %} + + {% block content %}{% endblock %} +
+ +
+

Django SaaS App

+
+ + {% block extra_js %}{% endblock %} + + diff --git a/apps/error-tracking/no-posthog-django/django3-saas/templates/billing/cancel.html b/apps/error-tracking/no-posthog-django/django3-saas/templates/billing/cancel.html new file mode 100644 index 000000000..9e11d38a2 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/templates/billing/cancel.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} + +{% block title %}Cancel Subscription{% endblock %} + +{% block content %} +

Cancel Subscription

+ +
+

Are you sure?

+

+ You're about to cancel your {{ subscription.plan.name }} subscription. +

+

+ You will continue to have access until {{ subscription.current_period_end|date:"M d, Y" }}. +

+ +
+ {% csrf_token %} + + Keep Subscription +
+
+{% endblock %} diff --git a/apps/error-tracking/no-posthog-django/django3-saas/templates/billing/change_plan.html b/apps/error-tracking/no-posthog-django/django3-saas/templates/billing/change_plan.html new file mode 100644 index 000000000..5df476a59 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/templates/billing/change_plan.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} + +{% block title %}Change Plan{% endblock %} + +{% block content %} +

Change Plan

+ +
+

+ Current Plan: {{ subscription.plan.name }} ({{ subscription.plan.get_price_display }})
+ New Plan: {{ plan.name }} ({{ plan.get_price_display }}) +

+ +
+ {% csrf_token %} +

+ Your plan will be changed immediately. +

+ + Cancel +
+
+{% endblock %} diff --git a/apps/error-tracking/no-posthog-django/django3-saas/templates/billing/manage.html b/apps/error-tracking/no-posthog-django/django3-saas/templates/billing/manage.html new file mode 100644 index 000000000..d94a59989 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/templates/billing/manage.html @@ -0,0 +1,46 @@ +{% extends 'base.html' %} + +{% block title %}Manage Subscription{% endblock %} + +{% block content %} +

Billing

+ +{% if subscription %} +
+

Current Subscription

+

+ Plan: {{ subscription.plan.name }}
+ Status: + + {{ subscription.get_status_display }} +
+ Price: {{ subscription.plan.get_price_display }}
+ Current Period: {{ subscription.current_period_start|date:"M d, Y" }} - {{ subscription.current_period_end|date:"M d, Y" }} +

+ + +
+ +

Change Plan

+
+ {% for plan in plans %} + {% if plan != subscription.plan %} +
+

{{ plan.name }}

+

{{ plan.get_price_display }}

+ Switch +
+ {% endif %} + {% endfor %} +
+ +{% else %} +
+

No Active Subscription

+

Choose a plan to get started.

+ View Plans +
+{% endif %} +{% endblock %} diff --git a/apps/error-tracking/no-posthog-django/django3-saas/templates/billing/pricing.html b/apps/error-tracking/no-posthog-django/django3-saas/templates/billing/pricing.html new file mode 100644 index 000000000..d8045564c --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/templates/billing/pricing.html @@ -0,0 +1,53 @@ +{% extends 'base.html' %} + +{% block title %}Pricing - Django SaaS{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +

Simple, Transparent Pricing

+

Choose the plan that fits your needs.

+ +
+ {% for plan in plans %} + + {% empty %} +

No plans available.

+ {% endfor %} +
+{% endblock %} diff --git a/apps/error-tracking/no-posthog-django/django3-saas/templates/billing/subscribe.html b/apps/error-tracking/no-posthog-django/django3-saas/templates/billing/subscribe.html new file mode 100644 index 000000000..2b40bb727 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/templates/billing/subscribe.html @@ -0,0 +1,30 @@ +{% extends 'base.html' %} + +{% block title %}Subscribe to {{ plan.name }}{% endblock %} + +{% block content %} +

Subscribe to {{ plan.name }}

+ +
+

{{ plan.name }} - {{ plan.get_price_display }}

+ {% if plan.description %} +

{{ plan.description }}

+ {% endif %} + +

Features

+
    + {% for feature in plan.features %} +
  • {{ feature }}
  • + {% endfor %} +
+ +
+ {% csrf_token %} +

+ By subscribing, you agree to our terms of service. +

+ + Cancel +
+
+{% endblock %} diff --git a/apps/error-tracking/no-posthog-django/django3-saas/templates/dashboard/create_project.html b/apps/error-tracking/no-posthog-django/django3-saas/templates/dashboard/create_project.html new file mode 100644 index 000000000..38d627f6a --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/templates/dashboard/create_project.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} + +{% block title %}Create Project{% endblock %} + +{% block content %} +

Create Project

+ +
+
+ {% csrf_token %} + {{ form.as_p }} + + Cancel +
+
+{% endblock %} diff --git a/apps/error-tracking/no-posthog-django/django3-saas/templates/dashboard/delete_project.html b/apps/error-tracking/no-posthog-django/django3-saas/templates/dashboard/delete_project.html new file mode 100644 index 000000000..06a4c12b3 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/templates/dashboard/delete_project.html @@ -0,0 +1,18 @@ +{% extends 'base.html' %} + +{% block title %}Delete Project{% endblock %} + +{% block content %} +

Delete Project

+ +
+

Are you sure you want to delete {{ project.name }}?

+

This action cannot be undone.

+ +
+ {% csrf_token %} + + Cancel +
+
+{% endblock %} diff --git a/apps/error-tracking/no-posthog-django/django3-saas/templates/dashboard/edit_project.html b/apps/error-tracking/no-posthog-django/django3-saas/templates/dashboard/edit_project.html new file mode 100644 index 000000000..5ac1bd361 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/templates/dashboard/edit_project.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} + +{% block title %}Edit Project{% endblock %} + +{% block content %} +

Edit Project

+ +
+
+ {% csrf_token %} + {{ form.as_p }} + + Cancel +
+
+{% endblock %} diff --git a/apps/error-tracking/no-posthog-django/django3-saas/templates/dashboard/index.html b/apps/error-tracking/no-posthog-django/django3-saas/templates/dashboard/index.html new file mode 100644 index 000000000..3162eef2e --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/templates/dashboard/index.html @@ -0,0 +1,114 @@ +{% extends 'base.html' %} + +{% block title %}Dashboard{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +

Dashboard

+ +
+
+

{{ metrics.project_count }}

+

Total Projects

+
+
+

{{ metrics.active_projects }}

+

Active Projects

+
+
+

{% if subscription %}{{ subscription.plan.name }}{% else %}Free{% endif %}

+

Current Plan

+
+ {% if subscription %} +
+

{{ metrics.days_remaining }}

+

Days Remaining

+
+ {% endif %} +
+

{{ metrics.activities_this_month }}

+

Actions This Month

+
+
+ +{% if not subscription %} +
+
+
+

Upgrade Your Plan

+

Get more projects and features with a paid plan.

+
+ View Plans +
+
+{% endif %} + +
+
+
+

Recent Projects

+ View All +
+ + {% if projects %} + {% for project in projects %} +
+
+

{{ project.name }}

+ + {% if project.is_active %}Active{% else %}Inactive{% endif %} + +
+ {% if project.description %} +

{{ project.description|truncatewords:20 }}

+ {% endif %} + Created {{ project.created_at|date:"M d, Y" }} +
+ {% endfor %} + {% else %} +
+

No projects yet.

+ Create your first project +
+ {% endif %} +
+ +
+

Recent Activity

+ {% if activities %} + {% for activity in activities %} +
+

{{ activity.description }}

+ {{ activity.created_at|date:"M d, Y H:i" }} +
+ {% endfor %} + {% else %} +

No recent activity.

+ {% endif %} + + {% if subscription %} +

Subscription

+
+

Plan: {{ subscription.plan.name }}

+

Status: + + {{ subscription.get_status_display }} + +

+

Renews: {{ subscription.current_period_end|date:"M d, Y" }}

+ Manage +
+ {% endif %} +
+
+{% endblock %} diff --git a/apps/error-tracking/no-posthog-django/django3-saas/templates/dashboard/projects.html b/apps/error-tracking/no-posthog-django/django3-saas/templates/dashboard/projects.html new file mode 100644 index 000000000..e9752a485 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/templates/dashboard/projects.html @@ -0,0 +1,38 @@ +{% extends 'base.html' %} + +{% block title %}Projects{% endblock %} + +{% block content %} +
+

Projects

+ New Project +
+ +{% if projects %} + {% for project in projects %} +
+
+
+

{{ project.name }}

+ {% if project.description %} +

{{ project.description|truncatewords:30 }}

+ {% endif %} + Created {{ project.created_at|date:"M d, Y" }} +
+
+ + {% if project.is_active %}Active{% else %}Inactive{% endif %} + + Edit + Delete +
+
+
+ {% endfor %} +{% else %} +
+

No projects yet.

+ Create your first project +
+{% endif %} +{% endblock %} diff --git a/apps/error-tracking/no-posthog-django/django3-saas/templates/errors/404.html b/apps/error-tracking/no-posthog-django/django3-saas/templates/errors/404.html new file mode 100644 index 000000000..04a38f32c --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/templates/errors/404.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block title %}Page Not Found - Django SaaS{% endblock %} + +{% block content %} +
+

404

+

Page Not Found

+

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

+ Go Home +
+{% endblock %} diff --git a/apps/error-tracking/no-posthog-django/django3-saas/templates/errors/500.html b/apps/error-tracking/no-posthog-django/django3-saas/templates/errors/500.html new file mode 100644 index 000000000..9e5dd1db1 --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/templates/errors/500.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block title %}Server Error - Django SaaS{% endblock %} + +{% block content %} +
+

500

+

Server Error

+

Something went wrong on our end. Please try again later.

+ Go Home +
+{% endblock %} diff --git a/apps/error-tracking/no-posthog-django/django3-saas/templates/marketing/features.html b/apps/error-tracking/no-posthog-django/django3-saas/templates/marketing/features.html new file mode 100644 index 000000000..148aeb69b --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/templates/marketing/features.html @@ -0,0 +1,44 @@ +{% extends 'base.html' %} + +{% block title %}Features - Django SaaS{% endblock %} + +{% block content %} +

Features

+

Everything you need to manage and scale your business.

+ +
+
+

Project Management

+

Create, organize, and manage all your projects from a central dashboard.

+
+ +
+

Activity Tracking

+

Keep track of all changes with detailed activity logs and history.

+
+ +
+

Subscription Billing

+

Flexible subscription plans with easy upgrade and downgrade options.

+
+ +
+

User Management

+

Manage your account settings, profile, and security preferences.

+
+ +
+

Admin Dashboard

+

Full admin panel to manage users, plans, and subscriptions.

+
+ +
+

Secure & Reliable

+

Built with security best practices. Your data is safe with us.

+
+
+ + +{% endblock %} diff --git a/apps/error-tracking/no-posthog-django/django3-saas/templates/marketing/home.html b/apps/error-tracking/no-posthog-django/django3-saas/templates/marketing/home.html new file mode 100644 index 000000000..e5ad8aadf --- /dev/null +++ b/apps/error-tracking/no-posthog-django/django3-saas/templates/marketing/home.html @@ -0,0 +1,38 @@ +{% extends 'base.html' %} + +{% block title %}Django SaaS - Home{% endblock %} + +{% block content %} +
+

Build Better Products Faster

+

+ A modern SaaS platform to manage your projects, track progress, and scale your business. +

+
+ {% if user.is_authenticated %} + Go to Dashboard + {% else %} + Start Free Trial + Login + {% endif %} +
+
+ +
+

Why Choose Us?

+
+
+

Project Management

+

Organize and track all your projects in one place with our intuitive dashboard.

+
+
+

Flexible Plans

+

Choose from multiple pricing tiers that grow with your business needs.

+
+
+

Activity Tracking

+

Monitor your team's progress with detailed activity logs and metrics.

+
+
+
+{% endblock %} diff --git a/apps/error-tracking/no-posthog-javascript-node/express-todo/.gitignore b/apps/error-tracking/no-posthog-javascript-node/express-todo/.gitignore new file mode 100644 index 000000000..e6367a1fe --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-node/express-todo/.gitignore @@ -0,0 +1,3 @@ +node_modules +.env +dist diff --git a/apps/error-tracking/no-posthog-javascript-node/express-todo/index.js b/apps/error-tracking/no-posthog-javascript-node/express-todo/index.js new file mode 100644 index 000000000..4ba2567bf --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-node/express-todo/index.js @@ -0,0 +1,53 @@ +const express = require('express'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +app.use(express.json()); + +const todos = []; +let nextId = 1; + +app.get('/api/todos', (req, res) => { + res.json(todos); +}); + +app.post('/api/todos', (req, res) => { + const { title } = req.body; + + if (!title) { + return res.status(400).json({ error: 'title is required' }); + } + + const todo = { id: nextId++, title, completed: false }; + todos.push(todo); + res.status(201).json(todo); +}); + +app.patch('/api/todos/:id', (req, res) => { + const todo = todos.find((t) => t.id === parseInt(req.params.id)); + + if (!todo) { + return res.status(404).json({ error: 'Not found' }); + } + + if (req.body.title !== undefined) todo.title = req.body.title; + if (req.body.completed !== undefined) todo.completed = req.body.completed; + + res.json(todo); +}); + +app.delete('/api/todos/:id', (req, res) => { + const index = todos.findIndex((t) => t.id === parseInt(req.params.id)); + + if (index === -1) { + return res.status(404).json({ error: 'Not found' }); + } + + todos.splice(index, 1); + res.status(204).send(); +}); + +app.listen(PORT, () => { + console.log(`Express todo API running on http://localhost:${PORT}`); +}); diff --git a/apps/error-tracking/no-posthog-javascript-node/express-todo/package.json b/apps/error-tracking/no-posthog-javascript-node/express-todo/package.json new file mode 100644 index 000000000..3c23d62f9 --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-node/express-todo/package.json @@ -0,0 +1,18 @@ +{ + "name": "express-todo", + "version": "1.0.0", + "description": "An Express todo API for testing the javascript_node wizard skill", + "main": "index.js", + "scripts": { + "start": "node index.js", + "start:production": "node dist/index.js", + "build": "esbuild index.js --bundle --platform=node --format=cjs --packages=external --sourcemap --sources-content=true --outfile=dist/index.js", + "dev": "node --watch index.js" + }, + "dependencies": { + "express": "^4.21.0" + }, + "devDependencies": { + "esbuild": "^0.28.2" + } +} diff --git a/apps/error-tracking/no-posthog-javascript-node/fastify-blog/.gitignore b/apps/error-tracking/no-posthog-javascript-node/fastify-blog/.gitignore new file mode 100644 index 000000000..e6367a1fe --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-node/fastify-blog/.gitignore @@ -0,0 +1,3 @@ +node_modules +.env +dist diff --git a/apps/error-tracking/no-posthog-javascript-node/fastify-blog/index.js b/apps/error-tracking/no-posthog-javascript-node/fastify-blog/index.js new file mode 100644 index 000000000..e2d298747 --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-node/fastify-blog/index.js @@ -0,0 +1,124 @@ +import Fastify from 'fastify'; + +const fastify = Fastify({ logger: true }); + +const posts = []; +const comments = []; +let nextPostId = 1; +let nextCommentId = 1; + +// List posts (with optional author filter and pagination) +fastify.get('/api/posts', async (request) => { + let result = posts; + const { author, limit = 20, offset = 0 } = request.query; + + if (author) { + result = result.filter((p) => p.author === author); + } + + return { + posts: result.slice(Number(offset), Number(offset) + Number(limit)), + total: result.length, + }; +}); + +// Create a post +fastify.post('/api/posts', async (request, reply) => { + const { title, body, author } = request.body || {}; + + if (!title || !body || !author) { + return reply.status(400).send({ error: 'title, body, and author are required' }); + } + + const post = { + id: nextPostId++, + title, + body, + author, + published: false, + created_at: new Date().toISOString(), + }; + posts.push(post); + return reply.status(201).send(post); +}); + +// Get a single post with its comments +fastify.get('/api/posts/:id', async (request, reply) => { + const post = posts.find((p) => p.id === parseInt(request.params.id, 10)); + + if (!post) { + return reply.status(404).send({ error: 'Post not found' }); + } + + const postComments = comments.filter((c) => c.post_id === post.id); + return { ...post, comments: postComments }; +}); + +// Update a post +fastify.patch('/api/posts/:id', async (request, reply) => { + const post = posts.find((p) => p.id === parseInt(request.params.id, 10)); + + if (!post) { + return reply.status(404).send({ error: 'Post not found' }); + } + + const { title, body, published } = request.body || {}; + if (title !== undefined) post.title = title; + if (body !== undefined) post.body = body; + if (published !== undefined) post.published = published; + + return post; +}); + +// Delete a post and its comments +fastify.delete('/api/posts/:id', async (request, reply) => { + const index = posts.findIndex((p) => p.id === parseInt(request.params.id, 10)); + + if (index === -1) { + return reply.status(404).send({ error: 'Post not found' }); + } + + const postId = posts[index].id; + posts.splice(index, 1); + + // Remove associated comments + for (let i = comments.length - 1; i >= 0; i--) { + if (comments[i].post_id === postId) comments.splice(i, 1); + } + + return reply.status(204).send(); +}); + +// Add a comment to a post +fastify.post('/api/posts/:id/comments', async (request, reply) => { + const post = posts.find((p) => p.id === parseInt(request.params.id, 10)); + + if (!post) { + return reply.status(404).send({ error: 'Post not found' }); + } + + const { author, body } = request.body || {}; + + if (!author || !body) { + return reply.status(400).send({ error: 'author and body are required' }); + } + + const comment = { + id: nextCommentId++, + post_id: post.id, + author, + body, + created_at: new Date().toISOString(), + }; + comments.push(comment); + return reply.status(201).send(comment); +}); + +const PORT = process.env.PORT || 3001; + +fastify.listen({ port: PORT }, (err) => { + if (err) { + fastify.log.error(err); + process.exit(1); + } +}); diff --git a/apps/error-tracking/no-posthog-javascript-node/fastify-blog/package.json b/apps/error-tracking/no-posthog-javascript-node/fastify-blog/package.json new file mode 100644 index 000000000..4d7db1637 --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-node/fastify-blog/package.json @@ -0,0 +1,19 @@ +{ + "name": "fastify-blog", + "version": "1.0.0", + "type": "module", + "description": "A Fastify blog API for testing the javascript_node wizard skill", + "main": "index.js", + "scripts": { + "start": "node index.js", + "start:production": "node dist/index.js", + "build": "esbuild index.js --bundle --platform=node --format=esm --packages=external --sourcemap --sources-content=true --outfile=dist/index.js", + "dev": "node --watch index.js" + }, + "dependencies": { + "fastify": "^5.2.0" + }, + "devDependencies": { + "esbuild": "^0.28.2" + } +} diff --git a/apps/error-tracking/no-posthog-javascript-node/hono-links/.gitignore b/apps/error-tracking/no-posthog-javascript-node/hono-links/.gitignore new file mode 100644 index 000000000..e6367a1fe --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-node/hono-links/.gitignore @@ -0,0 +1,3 @@ +node_modules +.env +dist diff --git a/apps/error-tracking/no-posthog-javascript-node/hono-links/index.js b/apps/error-tracking/no-posthog-javascript-node/hono-links/index.js new file mode 100644 index 000000000..21f35beeb --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-node/hono-links/index.js @@ -0,0 +1,109 @@ +import { Hono } from 'hono'; +import { serve } from '@hono/node-server'; + +const app = new Hono(); + +const links = []; +let nextId = 1; + +// List links (with optional tag filter and search) +app.get('/api/links', (c) => { + let result = links; + const tag = c.req.query('tag'); + const search = c.req.query('search'); + const favoritesOnly = c.req.query('favorites'); + + if (tag) { + result = result.filter((l) => l.tags.includes(tag)); + } + if (search) { + const q = search.toLowerCase(); + result = result.filter( + (l) => l.title.toLowerCase().includes(q) || l.url.toLowerCase().includes(q) + ); + } + if (favoritesOnly === 'true') { + result = result.filter((l) => l.favorite); + } + + return c.json({ links: result, total: result.length }); +}); + +// Save a new link +app.post('/api/links', async (c) => { + const { url, title, tags = [], description = '' } = await c.req.json(); + + if (!url || !title) { + return c.json({ error: 'url and title are required' }, 400); + } + + const link = { + id: nextId++, + url, + title, + description, + tags, + favorite: false, + created_at: new Date().toISOString(), + }; + links.push(link); + return c.json(link, 201); +}); + +// Get a single link +app.get('/api/links/:id', (c) => { + const link = links.find((l) => l.id === parseInt(c.req.param('id'), 10)); + + if (!link) { + return c.json({ error: 'Link not found' }, 404); + } + + return c.json(link); +}); + +// Update a link +app.patch('/api/links/:id', async (c) => { + const link = links.find((l) => l.id === parseInt(c.req.param('id'), 10)); + + if (!link) { + return c.json({ error: 'Link not found' }, 404); + } + + const body = await c.req.json(); + if (body.url !== undefined) link.url = body.url; + if (body.title !== undefined) link.title = body.title; + if (body.description !== undefined) link.description = body.description; + if (body.tags !== undefined) link.tags = body.tags; + if (body.favorite !== undefined) link.favorite = body.favorite; + + return c.json(link); +}); + +// Delete a link +app.delete('/api/links/:id', (c) => { + const index = links.findIndex((l) => l.id === parseInt(c.req.param('id'), 10)); + + if (index === -1) { + return c.json({ error: 'Link not found' }, 404); + } + + links.splice(index, 1); + return c.body(null, 204); +}); + +// List all tags +app.get('/api/tags', (c) => { + const tagCounts = {}; + for (const link of links) { + for (const tag of link.tags) { + tagCounts[tag] = (tagCounts[tag] || 0) + 1; + } + } + return c.json({ tags: tagCounts }); +}); + +const PORT = process.env.PORT || 3002; + +serve({ fetch: app.fetch, port: PORT }, () => { + console.log(`Hono links API running on http://localhost:${PORT}`); +}); diff --git a/apps/error-tracking/no-posthog-javascript-node/hono-links/package.json b/apps/error-tracking/no-posthog-javascript-node/hono-links/package.json new file mode 100644 index 000000000..d37409d0b --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-node/hono-links/package.json @@ -0,0 +1,20 @@ +{ + "name": "hono-links", + "version": "1.0.0", + "type": "module", + "description": "A Hono bookmark/link saver API for testing the javascript_node wizard skill", + "main": "index.js", + "scripts": { + "start": "node index.js", + "start:production": "node dist/index.js", + "build": "esbuild index.js --bundle --platform=node --format=esm --packages=external --sourcemap --sources-content=true --outfile=dist/index.js", + "dev": "node --watch index.js" + }, + "dependencies": { + "hono": "^4.6.0", + "@hono/node-server": "^1.13.0" + }, + "devDependencies": { + "esbuild": "^0.28.2" + } +} diff --git a/apps/error-tracking/no-posthog-javascript-node/koa-notes/.gitignore b/apps/error-tracking/no-posthog-javascript-node/koa-notes/.gitignore new file mode 100644 index 000000000..e6367a1fe --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-node/koa-notes/.gitignore @@ -0,0 +1,3 @@ +node_modules +.env +dist diff --git a/apps/error-tracking/no-posthog-javascript-node/koa-notes/index.js b/apps/error-tracking/no-posthog-javascript-node/koa-notes/index.js new file mode 100644 index 000000000..5394e6b19 --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-node/koa-notes/index.js @@ -0,0 +1,169 @@ +import Koa from 'koa'; +import Router from 'koa-router'; +import bodyParser from 'koa-bodyparser'; + +const app = new Koa(); +const router = new Router(); + +app.use(bodyParser()); + +const folders = [{ id: 1, name: 'General' }]; +const notes = []; +let nextFolderId = 2; +let nextNoteId = 1; + +// --- Folders --- + +router.get('/api/folders', (ctx) => { + ctx.body = folders.map((f) => ({ + ...f, + note_count: notes.filter((n) => n.folder_id === f.id).length, + })); +}); + +router.post('/api/folders', (ctx) => { + const { name } = ctx.request.body; + + if (!name) { + ctx.status = 400; + ctx.body = { error: 'name is required' }; + return; + } + + const folder = { id: nextFolderId++, name }; + folders.push(folder); + ctx.status = 201; + ctx.body = folder; +}); + +router.delete('/api/folders/:id', (ctx) => { + const folderId = parseInt(ctx.params.id, 10); + + if (folderId === 1) { + ctx.status = 400; + ctx.body = { error: 'Cannot delete the default folder' }; + return; + } + + const index = folders.findIndex((f) => f.id === folderId); + + if (index === -1) { + ctx.status = 404; + ctx.body = { error: 'Folder not found' }; + return; + } + + // Move notes from deleted folder to General + for (const note of notes) { + if (note.folder_id === folderId) note.folder_id = 1; + } + + folders.splice(index, 1); + ctx.status = 204; +}); + +// --- Notes --- + +router.get('/api/notes', (ctx) => { + let result = notes; + const { folder_id, search } = ctx.query; + + if (folder_id) { + result = result.filter((n) => n.folder_id === parseInt(folder_id, 10)); + } + if (search) { + const q = search.toLowerCase(); + result = result.filter( + (n) => n.title.toLowerCase().includes(q) || n.content.toLowerCase().includes(q) + ); + } + + ctx.body = { notes: result, total: result.length }; +}); + +router.post('/api/notes', (ctx) => { + const { title, content = '', folder_id = 1 } = ctx.request.body; + + if (!title) { + ctx.status = 400; + ctx.body = { error: 'title is required' }; + return; + } + + if (!folders.find((f) => f.id === folder_id)) { + ctx.status = 400; + ctx.body = { error: 'folder not found' }; + return; + } + + const note = { + id: nextNoteId++, + title, + content, + folder_id, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }; + notes.push(note); + ctx.status = 201; + ctx.body = note; +}); + +router.get('/api/notes/:id', (ctx) => { + const note = notes.find((n) => n.id === parseInt(ctx.params.id, 10)); + + if (!note) { + ctx.status = 404; + ctx.body = { error: 'Note not found' }; + return; + } + + ctx.body = note; +}); + +router.patch('/api/notes/:id', (ctx) => { + const note = notes.find((n) => n.id === parseInt(ctx.params.id, 10)); + + if (!note) { + ctx.status = 404; + ctx.body = { error: 'Note not found' }; + return; + } + + const { title, content, folder_id } = ctx.request.body; + if (title !== undefined) note.title = title; + if (content !== undefined) note.content = content; + if (folder_id !== undefined) { + if (!folders.find((f) => f.id === folder_id)) { + ctx.status = 400; + ctx.body = { error: 'folder not found' }; + return; + } + note.folder_id = folder_id; + } + note.updated_at = new Date().toISOString(); + + ctx.body = note; +}); + +router.delete('/api/notes/:id', (ctx) => { + const index = notes.findIndex((n) => n.id === parseInt(ctx.params.id, 10)); + + if (index === -1) { + ctx.status = 404; + ctx.body = { error: 'Note not found' }; + return; + } + + notes.splice(index, 1); + ctx.status = 204; +}); + +app.use(router.routes()); +app.use(router.allowedMethods()); + +const PORT = process.env.PORT || 3003; + +app.listen(PORT, () => { + console.log(`Koa notes API running on http://localhost:${PORT}`); +}); diff --git a/apps/error-tracking/no-posthog-javascript-node/koa-notes/package.json b/apps/error-tracking/no-posthog-javascript-node/koa-notes/package.json new file mode 100644 index 000000000..414d4f430 --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-node/koa-notes/package.json @@ -0,0 +1,21 @@ +{ + "name": "koa-notes", + "version": "1.0.0", + "type": "module", + "description": "A Koa notes API for testing the javascript_node wizard skill", + "main": "index.js", + "scripts": { + "start": "node index.js", + "start:production": "node dist/index.js", + "build": "esbuild index.js --bundle --platform=node --format=esm --packages=external --sourcemap --sources-content=true --outfile=dist/index.js", + "dev": "node --watch index.js" + }, + "dependencies": { + "koa": "^2.15.0", + "koa-router": "^13.0.0", + "koa-bodyparser": "^4.4.0" + }, + "devDependencies": { + "esbuild": "^0.28.2" + } +} diff --git a/apps/error-tracking/no-posthog-javascript-node/native-http-contacts/.gitignore b/apps/error-tracking/no-posthog-javascript-node/native-http-contacts/.gitignore new file mode 100644 index 000000000..e6367a1fe --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-node/native-http-contacts/.gitignore @@ -0,0 +1,3 @@ +node_modules +.env +dist diff --git a/apps/error-tracking/no-posthog-javascript-node/native-http-contacts/index.js b/apps/error-tracking/no-posthog-javascript-node/native-http-contacts/index.js new file mode 100644 index 000000000..0ef675101 --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-node/native-http-contacts/index.js @@ -0,0 +1,141 @@ +import { createServer } from 'node:http'; + +const contacts = []; +const groups = [{ id: 1, name: 'All Contacts' }]; +let nextContactId = 1; +let nextGroupId = 2; + +function parseBody(req) { + return new Promise((resolve, reject) => { + let data = ''; + req.on('data', (chunk) => (data += chunk)); + req.on('end', () => { + try { + resolve(data ? JSON.parse(data) : {}); + } catch { + reject(new Error('Invalid JSON')); + } + }); + req.on('error', reject); + }); +} + +function json(res, statusCode, data) { + res.writeHead(statusCode, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(data)); +} + +const server = createServer(async (req, res) => { + const url = new URL(req.url, `http://${req.headers.host}`); + const path = url.pathname; + const method = req.method; + + try { + // --- Groups --- + + if (method === 'GET' && path === '/api/groups') { + const result = groups.map((g) => ({ + ...g, + contact_count: contacts.filter((c) => c.group_id === g.id).length, + })); + return json(res, 200, result); + } + + if (method === 'POST' && path === '/api/groups') { + const body = await parseBody(req); + if (!body.name) return json(res, 400, { error: 'name is required' }); + + const group = { id: nextGroupId++, name: body.name }; + groups.push(group); + return json(res, 201, group); + } + + // --- Contacts --- + + if (method === 'GET' && path === '/api/contacts') { + let result = contacts; + const groupId = url.searchParams.get('group_id'); + const search = url.searchParams.get('search'); + + if (groupId) { + result = result.filter((c) => c.group_id === parseInt(groupId, 10)); + } + if (search) { + const q = search.toLowerCase(); + result = result.filter( + (c) => + c.name.toLowerCase().includes(q) || + c.email.toLowerCase().includes(q) || + (c.phone && c.phone.includes(q)) + ); + } + + return json(res, 200, { contacts: result, total: result.length }); + } + + if (method === 'POST' && path === '/api/contacts') { + const body = await parseBody(req); + + if (!body.name || !body.email) { + return json(res, 400, { error: 'name and email are required' }); + } + + const contact = { + id: nextContactId++, + name: body.name, + email: body.email, + phone: body.phone || null, + company: body.company || null, + group_id: body.group_id || 1, + created_at: new Date().toISOString(), + }; + contacts.push(contact); + return json(res, 201, contact); + } + + // GET /api/contacts/:id + const getMatch = method === 'GET' && path.match(/^\/api\/contacts\/(\d+)$/); + if (getMatch) { + const contact = contacts.find((c) => c.id === parseInt(getMatch[1], 10)); + if (!contact) return json(res, 404, { error: 'Contact not found' }); + return json(res, 200, contact); + } + + // PATCH /api/contacts/:id + const patchMatch = method === 'PATCH' && path.match(/^\/api\/contacts\/(\d+)$/); + if (patchMatch) { + const contact = contacts.find((c) => c.id === parseInt(patchMatch[1], 10)); + if (!contact) return json(res, 404, { error: 'Contact not found' }); + + const body = await parseBody(req); + if (body.name !== undefined) contact.name = body.name; + if (body.email !== undefined) contact.email = body.email; + if (body.phone !== undefined) contact.phone = body.phone; + if (body.company !== undefined) contact.company = body.company; + if (body.group_id !== undefined) contact.group_id = body.group_id; + + return json(res, 200, contact); + } + + // DELETE /api/contacts/:id + const deleteMatch = method === 'DELETE' && path.match(/^\/api\/contacts\/(\d+)$/); + if (deleteMatch) { + const index = contacts.findIndex((c) => c.id === parseInt(deleteMatch[1], 10)); + if (index === -1) return json(res, 404, { error: 'Contact not found' }); + + contacts.splice(index, 1); + res.writeHead(204); + return res.end(); + } + + json(res, 404, { error: 'Not found' }); + } catch (err) { + json(res, 500, { error: 'Internal server error' }); + } +}); + +const PORT = process.env.PORT || 3004; + +server.listen(PORT, () => { + console.log(`Native HTTP contacts API running on http://localhost:${PORT}`); +}); diff --git a/apps/error-tracking/no-posthog-javascript-node/native-http-contacts/package.json b/apps/error-tracking/no-posthog-javascript-node/native-http-contacts/package.json new file mode 100644 index 000000000..c87bd4552 --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-node/native-http-contacts/package.json @@ -0,0 +1,17 @@ +{ + "name": "native-http-contacts", + "version": "1.0.0", + "type": "module", + "description": "A native Node.js HTTP contacts API for testing the javascript_node wizard skill", + "main": "index.js", + "scripts": { + "start": "node index.js", + "start:production": "node dist/index.js", + "build": "esbuild index.js --bundle --platform=node --format=esm --packages=external --sourcemap --sources-content=true --outfile=dist/index.js", + "dev": "node --watch index.js" + }, + "dependencies": {}, + "devDependencies": { + "esbuild": "^0.28.2" + } +} diff --git a/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/.env.example b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/.env.example new file mode 100644 index 000000000..c1e25270a --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/.env.example @@ -0,0 +1,6 @@ +# PostHog Configuration +VITE_POSTHOG_KEY=phc_your_project_api_key_here +VITE_POSTHOG_HOST=https://us.i.posthog.com + +# App Configuration +VITE_APP_NAME=TrackFlow diff --git a/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/.gitignore b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/.gitignore new file mode 100644 index 000000000..ae3897d9d --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.env +.env.local +.DS_Store diff --git a/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/README.md b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/README.md new file mode 100644 index 000000000..f62ddf241 --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/README.md @@ -0,0 +1,38 @@ +# TrackFlow — SaaS Project Management Dashboard + +A production-style vanilla JavaScript SPA built with Vite. Used as a test fixture for the PostHog wizard's general JavaScript skill detection. + +## Stack + +- **Vanilla JavaScript** (no frameworks) +- **Vite** bundler +- **chart.js** dependency (unused but present for realistic package.json) +- Hash-based SPA router +- localStorage-backed state + +## Features + +- Login/auth flow with 3 demo accounts +- Dashboard with stats, progress bars, team workload +- Projects list with create/delete +- Kanban board with task management (add, move, assign, delete) +- Settings with theme toggle, notifications, team members +- Responsive sidebar + topbar layout +- Dark mode support + +## Running locally + +```bash +npm install +npm run dev +``` + +Open http://localhost:5173 and sign in with `alice@trackflow.dev`. + +## Demo accounts + +| Email | Role | +| ---------------------- | ------ | +| alice@trackflow.dev | Admin | +| bob@trackflow.dev | Member | +| carol@trackflow.dev | Member | diff --git a/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/index.html b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/index.html new file mode 100644 index 000000000..5f94736ff --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/index.html @@ -0,0 +1,14 @@ + + + + + + TrackFlow — Project Management + + + + +
+ + + diff --git a/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/package-lock.json b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/package-lock.json new file mode 100644 index 000000000..05bcd0830 --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/package-lock.json @@ -0,0 +1,1126 @@ +{ + "name": "trackflow", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "trackflow", + "version": "1.0.0", + "dependencies": { + "chart.js": "^4.4.0" + }, + "devDependencies": { + "vite": "^6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "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/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/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/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": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.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 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/package.json b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/package.json new file mode 100644 index 000000000..c11a7b4b6 --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/package.json @@ -0,0 +1,17 @@ +{ + "name": "trackflow", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "chart.js": "^4.4.0" + }, + "devDependencies": { + "vite": "^6.0.0" + } +} diff --git a/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/api.js b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/api.js new file mode 100644 index 000000000..d4e69aac8 --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/api.js @@ -0,0 +1,103 @@ +/** + * Simulated async API layer. + * + * Wraps store operations in Promises with artificial delay + * to mimic real network calls. In a real app, these would be + * fetch() calls to a backend. + */ +import { store } from './store.js'; + +const DELAY_MS = 150; + +function delay(ms = DELAY_MS) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export const api = { + async login(email) { + await delay(); + + const success = store.login(email); + if (!success) { + throw new Error('Invalid credentials. Use a team member email.'); + } + return store.state.currentUser; + }, + + async logout() { + await delay(50); + store.logout(); + }, + + async getProjects() { + await delay(); + return store.state.projects; + }, + + async getProject(id) { + await delay(); + const project = store.getProject(id); + if (!project) throw new Error(`Project ${id} not found`); + return project; + }, + + async createProject(name, description) { + await delay(); + + if (!name.trim()) throw new Error('Project name is required'); + return store.createProject(name.trim(), description.trim()); + }, + + async deleteProject(id) { + await delay(); + store.deleteProject(id); + }, + + async addTask(projectId, title, priority) { + await delay(); + + if (!title.trim()) throw new Error('Task title is required'); + return store.addTask(projectId, title.trim(), priority); + }, + + async updateTaskStatus(projectId, taskId, status) { + await delay(50); + store.updateTaskStatus(projectId, taskId, status); + }, + + async deleteTask(projectId, taskId) { + await delay(50); + store.deleteTask(projectId, taskId); + }, + + async assignTask(projectId, taskId, assigneeId) { + await delay(50); + store.assignTask(projectId, taskId, assigneeId); + }, + + async getStats() { + await delay(); + return store.getStats(); + }, + + async getTeamMembers() { + await delay(); + return store.state.teamMembers; + }, + + async updateSettings(updates) { + await delay(); + store.updateSettings(updates); + return store.state.settings; + }, + + async getSettings() { + await delay(); + return store.state.settings; + }, + + async getActivities() { + await delay(); + return store.getActivities(); + }, +}; diff --git a/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/components/modal.js b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/components/modal.js new file mode 100644 index 000000000..865210768 --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/components/modal.js @@ -0,0 +1,53 @@ +/** + * Reusable modal dialog. + * + * Usage: + * showModal('Title', '

HTML body

', (modalEl) => { + * // attach event listeners to modalEl + * }); + */ +export function showModal(title, bodyHtml, onMount) { + // Remove any existing modal + document.getElementById('modal-overlay')?.remove(); + + const overlay = document.createElement('div'); + overlay.id = 'modal-overlay'; + overlay.className = 'modal-overlay'; + + overlay.innerHTML = ` + + `; + + document.body.appendChild(overlay); + + // Close on overlay click + overlay.addEventListener('click', (e) => { + if (e.target === overlay) overlay.remove(); + }); + + // Close on X button + overlay.querySelector('.modal-close').addEventListener('click', () => { + overlay.remove(); + }); + + // Close on Escape + const onKeydown = (e) => { + if (e.key === 'Escape') { + overlay.remove(); + document.removeEventListener('keydown', onKeydown); + } + }; + document.addEventListener('keydown', onKeydown); + + if (onMount) { + onMount(overlay); + } +} diff --git a/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/components/shell.js b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/components/shell.js new file mode 100644 index 000000000..7b7502aa9 --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/components/shell.js @@ -0,0 +1,59 @@ +import { api } from '../api.js'; +import { router } from '../router.js'; +import { store } from '../store.js'; + +/** + * Renders the app shell: sidebar + header + content area. + * Highlights the active nav item based on `activeSection`. + */ +export function renderShell(activeSection) { + const app = document.getElementById('app'); + const user = store.state.currentUser; + + app.innerHTML = ` +
+ + +
+
+
+ Welcome, ${user?.name?.split(' ')[0] || 'User'} +
+
+ ${user?.avatar || '??'} + +
+
+ +
+
+
+ `; + + document.getElementById('logout-btn').addEventListener('click', async () => { + await api.logout(); + router.navigate('/login'); + }); +} diff --git a/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/main.js b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/main.js new file mode 100644 index 000000000..1036566af --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/main.js @@ -0,0 +1,42 @@ +import { router } from './router.js'; +import { store } from './store.js'; +import { renderLogin } from './pages/login.js'; +import { renderDashboard } from './pages/dashboard.js'; +import { renderProjects } from './pages/projects.js'; +import { renderProjectDetail } from './pages/project-detail.js'; +import { renderSettings } from './pages/settings.js'; +import { renderActivity } from './pages/activity.js'; + +/** + * Auth guard — redirects to login if no user is logged in. + */ +function requireAuth(handler) { + return (params) => { + if (!store.state.currentUser) { + router.navigate('/login'); + return; + } + handler(params); + }; +} + +// --- Routes --- + +router.on('/login', renderLogin); +router.on('/dashboard', requireAuth(renderDashboard)); +router.on('/projects', requireAuth(renderProjects)); +router.on('/projects/:id', requireAuth(renderProjectDetail)); +router.on('/activity', requireAuth(renderActivity)); +router.on('/settings', requireAuth(renderSettings)); + +router.notFound(() => { + if (store.state.currentUser) { + router.navigate('/dashboard'); + } else { + router.navigate('/login'); + } +}); + +// --- Start --- + +router.start(); diff --git a/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/pages/activity.js b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/pages/activity.js new file mode 100644 index 000000000..7586eecb9 --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/pages/activity.js @@ -0,0 +1,74 @@ +import { api } from '../api.js'; +import { store } from '../store.js'; +import { renderShell } from '../components/shell.js'; + +const ACTION_META = { + created_project: { icon: '➕', label: 'Created project', color: 'var(--color-primary)' }, + deleted_project: { icon: '✖', label: 'Deleted project', color: 'var(--color-danger)' }, + added_task: { icon: '➕', label: 'Added task', color: 'var(--color-success)' }, + completed_task: { icon: '✔', label: 'Completed task', color: 'var(--color-success)' }, + moved_task: { icon: '➡', label: 'Moved task', color: 'var(--color-warning)' }, + deleted_task: { icon: '✖', label: 'Deleted task', color: 'var(--color-danger)' }, + assigned_task: { icon: '👤', label: 'Assigned task', color: 'var(--color-primary)' }, + logged_in: { icon: '→', label: 'Signed in', color: 'var(--color-text-muted)' }, +}; + +function formatTime(ts) { + const d = new Date(ts); + const now = new Date(); + const diffMs = now - d; + const diffMins = Math.floor(diffMs / 60000); + const diffHrs = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMins < 1) return 'Just now'; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffHrs < 24) return `${diffHrs}h ago`; + if (diffDays < 7) return `${diffDays}d ago`; + return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); +} + +function getMemberName(userId) { + const member = store.state.teamMembers.find((m) => m.id === userId); + return member?.name || userId; +} + +export async function renderActivity() { + renderShell('activity'); + + const content = document.getElementById('content'); + content.innerHTML = '
Loading activity...
'; + + try { + const activities = await api.getActivities(); + + content.innerHTML = ` + + +
+ ${activities.length === 0 + ? '

No activity yet.

' + : activities + .map((a) => { + const meta = ACTION_META[a.action] || { icon: '○', label: a.action, color: 'var(--color-text-muted)' }; + return ` +
+
${meta.icon}
+
+ ${getMemberName(a.user)} + ${meta.label.toLowerCase()} + ${a.detail} +
+ ${formatTime(a.timestamp)} +
+ `; + }) + .join('')} +
+ `; + } catch (err) { + content.innerHTML = `
Failed to load activity: ${err.message}
`; + } +} diff --git a/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/pages/dashboard.js b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/pages/dashboard.js new file mode 100644 index 000000000..f0fa0a85b --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/pages/dashboard.js @@ -0,0 +1,219 @@ +import { Chart, DoughnutController, ArcElement, Tooltip, Legend, BarController, BarElement, CategoryScale, LinearScale } from 'chart.js'; +import { api } from '../api.js'; +import { store } from '../store.js'; +import { renderShell } from '../components/shell.js'; + +Chart.register(DoughnutController, ArcElement, Tooltip, Legend, BarController, BarElement, CategoryScale, LinearScale); + +const ACTION_META = { + created_project: { icon: '➕', label: 'Created project', color: 'var(--color-primary)' }, + deleted_project: { icon: '✖', label: 'Deleted project', color: 'var(--color-danger)' }, + added_task: { icon: '➕', label: 'Added task', color: 'var(--color-success)' }, + completed_task: { icon: '✔', label: 'Completed task', color: 'var(--color-success)' }, + moved_task: { icon: '➡', label: 'Moved task', color: 'var(--color-warning)' }, + deleted_task: { icon: '✖', label: 'Deleted task', color: 'var(--color-danger)' }, + assigned_task: { icon: '👤', label: 'Assigned task', color: 'var(--color-primary)' }, + logged_in: { icon: '→', label: 'Signed in', color: 'var(--color-text-muted)' }, +}; + +function formatTime(ts) { + const d = new Date(ts); + const now = new Date(); + const diffMs = now - d; + const diffMins = Math.floor(diffMs / 60000); + const diffHrs = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMins < 1) return 'Just now'; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffHrs < 24) return `${diffHrs}h ago`; + if (diffDays < 7) return `${diffDays}d ago`; + return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); +} + +function getMemberName(userId) { + const member = store.state.teamMembers.find((m) => m.id === userId); + return member?.name || userId; +} + +function isDark() { + return document.documentElement.getAttribute('data-theme') === 'dark'; +} + +export async function renderDashboard() { + renderShell('dashboard'); + + const content = document.getElementById('content'); + content.innerHTML = '
Loading dashboard...
'; + + try { + const [stats, projects, activities] = await Promise.all([ + api.getStats(), + api.getProjects(), + api.getActivities(), + ]); + const activeProjects = projects.filter((p) => p.status === 'active'); + const recentActivities = activities.slice(0, 5); + + content.innerHTML = ` + + +
+
+ ${stats.activeProjects} + Active Projects +
+
+ ${stats.totalTasks} + Total Tasks +
+
+ ${stats.completionRate}% + Completion Rate +
+
+ ${stats.inProgressTasks} + In Progress +
+
+ +
+
+

Task Breakdown

+
+ +
+
+ +
+

Team Workload

+
+ +
+
+
+ +
+
+
+

Recent Activity

+ View all → +
+ ${recentActivities.length === 0 + ? '

No activity yet.

' + : `
+ ${recentActivities + .map((a) => { + const meta = ACTION_META[a.action] || { icon: '○', label: a.action, color: 'var(--color-text-muted)' }; + return ` +
+
${meta.icon}
+
+ ${getMemberName(a.user)} + ${meta.label.toLowerCase()} + ${a.detail} +
+ ${formatTime(a.timestamp)} +
+ `; + }) + .join('')} +
`} +
+ +
+

Active Projects

+ +
+
+ `; + + // --- Render Charts --- + + const dark = isDark(); + const textColor = dark ? '#e5e7eb' : '#1a1a2e'; + const gridColor = dark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)'; + + // Doughnut — Task Breakdown + new Chart(document.getElementById('task-chart'), { + type: 'doughnut', + data: { + labels: ['Done', 'In Progress', 'To Do'], + datasets: [ + { + data: [stats.doneTasks, stats.inProgressTasks, stats.todoTasks], + backgroundColor: ['#22c55e', '#f59e0b', '#94a3b8'], + borderWidth: 0, + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + cutout: '60%', + plugins: { + legend: { + position: 'bottom', + labels: { color: textColor, padding: 16, usePointStyle: true, pointStyleWidth: 8 }, + }, + }, + }, + }); + + // Bar — Team Workload + new Chart(document.getElementById('team-chart'), { + type: 'bar', + data: { + labels: stats.tasksByMember.map((m) => m.name.split(' ')[0]), + datasets: [ + { + label: 'Tasks', + data: stats.tasksByMember.map((m) => m.count), + backgroundColor: '#4f46e5', + borderRadius: 4, + barThickness: 32, + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { display: false }, + }, + scales: { + y: { + beginAtZero: true, + ticks: { stepSize: 1, color: textColor }, + grid: { color: gridColor }, + }, + x: { + ticks: { color: textColor }, + grid: { display: false }, + }, + }, + }, + }); + } catch (err) { + content.innerHTML = `
Failed to load dashboard: ${err.message}
`; + } +} diff --git a/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/pages/login.js b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/pages/login.js new file mode 100644 index 000000000..ad700ace8 --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/pages/login.js @@ -0,0 +1,51 @@ +import { api } from '../api.js'; +import { router } from '../router.js'; + +export function renderLogin() { + const app = document.getElementById('app'); + app.innerHTML = ` + + `; + + const form = document.getElementById('login-form'); + const errorEl = document.getElementById('login-error'); + + form.addEventListener('submit', async (e) => { + e.preventDefault(); + errorEl.hidden = true; + + const email = document.getElementById('email').value.trim(); + const btn = form.querySelector('button'); + btn.disabled = true; + btn.textContent = 'Signing in...'; + + try { + await api.login(email); + router.navigate('/dashboard'); + } catch (err) { + errorEl.textContent = err.message; + errorEl.hidden = false; + btn.disabled = false; + btn.textContent = 'Sign In'; + } + }); +} diff --git a/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/pages/project-detail.js b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/pages/project-detail.js new file mode 100644 index 000000000..506deabf2 --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/pages/project-detail.js @@ -0,0 +1,169 @@ +import { api } from '../api.js'; +import { store } from '../store.js'; +import { renderShell } from '../components/shell.js'; +import { showModal } from '../components/modal.js'; + +const STATUS_OPTIONS = ['todo', 'in_progress', 'done']; +const PRIORITY_OPTIONS = ['low', 'medium', 'high']; + +export async function renderProjectDetail({ id }) { + renderShell('projects'); + + const content = document.getElementById('content'); + content.innerHTML = '
Loading project...
'; + + try { + const project = await api.getProject(id); + const members = await api.getTeamMembers(); + + render(project, members); + } catch (err) { + content.innerHTML = `
${err.message}
`; + } +} + +function render(project, members) { + const content = document.getElementById('content'); + + const tasksByStatus = { + todo: project.tasks.filter((t) => t.status === 'todo'), + in_progress: project.tasks.filter((t) => t.status === 'in_progress'), + done: project.tasks.filter((t) => t.status === 'done'), + }; + + content.innerHTML = ` + + +
+ ${STATUS_OPTIONS.map( + (status) => ` +
+
+

${formatStatus(status)}

+ ${tasksByStatus[status].length} +
+
+ ${tasksByStatus[status] + .map( + (task) => ` +
+
+ + ${task.title} +
+ +
+ `, + ) + .join('')} +
+
+ `, + ).join('')} +
+ `; + + // Add task + document.getElementById('add-task-btn').addEventListener('click', () => { + showModal('Add Task', ` +
+ + + + + +
+ `, (modalEl) => { + modalEl.querySelector('#add-task-form').addEventListener('submit', async (e) => { + e.preventDefault(); + const title = modalEl.querySelector('#task-title').value; + const priority = modalEl.querySelector('#task-priority').value; + + try { + await api.addTask(project.id, title, priority); + document.getElementById('modal-overlay')?.remove(); + const updated = await api.getProject(project.id); + render(updated, members); + } catch (err) { + alert(err.message); + } + }); + }); + }); + + // Move task status + content.querySelectorAll('.move-task').forEach((btn) => { + btn.addEventListener('click', async () => { + await api.updateTaskStatus(project.id, btn.dataset.taskId, btn.dataset.status); + const updated = await api.getProject(project.id); + render(updated, members); + }); + }); + + // Assign task + content.querySelectorAll('.assign-task').forEach((btn) => { + btn.addEventListener('click', () => { + showModal('Assign Task', ` +
+ + ${members.map((m) => ``).join('')} +
+ `, (modalEl) => { + modalEl.querySelectorAll('.assign-option').forEach((opt) => { + opt.addEventListener('click', async () => { + await api.assignTask(project.id, btn.dataset.taskId, opt.dataset.assignee || null); + document.getElementById('modal-overlay')?.remove(); + const updated = await api.getProject(project.id); + render(updated, members); + }); + }); + }); + }); + }); + + // Delete task + content.querySelectorAll('.delete-task').forEach((btn) => { + btn.addEventListener('click', async () => { + await api.deleteTask(project.id, btn.dataset.taskId); + const updated = await api.getProject(project.id); + render(updated, members); + }); + }); +} + +function formatStatus(status) { + return status.replace('_', ' ').replace(/\b\w/g, (c) => c.toUpperCase()); +} + +function statusIcon(status) { + switch (status) { + case 'todo': return '○'; + case 'in_progress': return '◔'; + case 'done': return '✓'; + default: return '?'; + } +} diff --git a/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/pages/projects.js b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/pages/projects.js new file mode 100644 index 000000000..5a56d3aba --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/pages/projects.js @@ -0,0 +1,100 @@ +import { api } from '../api.js'; +import { router } from '../router.js'; +import { renderShell } from '../components/shell.js'; +import { showModal } from '../components/modal.js'; + +export async function renderProjects() { + renderShell('projects'); + + const content = document.getElementById('content'); + content.innerHTML = '
Loading projects...
'; + + try { + const projects = await api.getProjects(); + + content.innerHTML = ` + + +
+
+ Name + Status + Tasks + Progress + Actions +
+ ${projects + .map((p) => { + const done = p.tasks.filter((t) => t.status === 'done').length; + const pct = p.tasks.length > 0 ? Math.round((done / p.tasks.length) * 100) : 0; + + return ` +
+ + ${p.name} + ${p.description} + + + ${p.status} + + ${p.tasks.length} + +
+
+
+ ${pct}% +
+ + + +
+ `; + }) + .join('')} +
+ `; + + // New project + document.getElementById('new-project-btn').addEventListener('click', () => { + showModal('New Project', ` +
+ + + + + +
+ `, (modalEl) => { + modalEl.querySelector('#new-project-form').addEventListener('submit', async (e) => { + e.preventDefault(); + const name = modalEl.querySelector('#proj-name').value; + const desc = modalEl.querySelector('#proj-desc').value; + + try { + const project = await api.createProject(name, desc); + router.navigate(`/projects/${project.id}`); + } catch (err) { + alert(err.message); + } + }); + }); + }); + + // Delete project + content.querySelectorAll('.delete-project').forEach((btn) => { + btn.addEventListener('click', async (e) => { + e.stopPropagation(); + const id = btn.dataset.id; + if (confirm('Delete this project and all its tasks?')) { + await api.deleteProject(id); + renderProjects(); + } + }); + }); + } catch (err) { + content.innerHTML = `
Failed to load projects: ${err.message}
`; + } +} diff --git a/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/pages/settings.js b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/pages/settings.js new file mode 100644 index 000000000..16bb76dab --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/pages/settings.js @@ -0,0 +1,109 @@ +import { api } from '../api.js'; +import { store } from '../store.js'; +import { renderShell } from '../components/shell.js'; + +export async function renderSettings() { + renderShell('settings'); + + const content = document.getElementById('content'); + content.innerHTML = '
Loading settings...
'; + + try { + const settings = await api.getSettings(); + const user = store.state.currentUser; + + content.innerHTML = ` + + +
+
+

Profile

+
+ + ${user.name} +
+
+ + ${user.email} +
+
+ + ${user.role} +
+
+ +
+

Preferences

+
+ + +
+
+ + +
+
+ + +
+
+ +
+

Team Members

+
+ ${store.state.teamMembers + .map( + (m) => ` +
+ ${m.avatar} +
+ ${m.name} + ${m.email} +
+ ${m.role} +
+ `, + ) + .join('')} +
+
+ +
+

Danger Zone

+ +
+
+ `; + + // Theme + document.getElementById('theme-select').addEventListener('change', async (e) => { + await api.updateSettings({ theme: e.target.value }); + document.body.dataset.theme = e.target.value; + }); + + // Notifications + document.getElementById('email-notif').addEventListener('change', async (e) => { + await api.updateSettings({ emailNotifications: e.target.checked }); + }); + + document.getElementById('weekly-digest').addEventListener('change', async (e) => { + await api.updateSettings({ weeklyDigest: e.target.checked }); + }); + + // Reset + document.getElementById('reset-data-btn').addEventListener('click', () => { + if (confirm('Reset all data to defaults? This cannot be undone.')) { + store.reset(); + store.login(user.email); + renderSettings(); + } + }); + } catch (err) { + content.innerHTML = `
Failed to load settings: ${err.message}
`; + } +} diff --git a/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/router.js b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/router.js new file mode 100644 index 000000000..28e7254e7 --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/router.js @@ -0,0 +1,74 @@ +/** + * Simple hash-based router for vanilla JS SPA. + * + * Usage: + * router.on('/dashboard', renderDashboard) + * router.on('/projects/:id', renderProject) + * router.navigate('/dashboard') + */ + +const routes = []; +let notFoundHandler = null; + +function matchRoute(path) { + for (const route of routes) { + const params = extractParams(route.pattern, path); + if (params !== null) { + return { handler: route.handler, params }; + } + } + return null; +} + +function extractParams(pattern, path) { + const patternParts = pattern.split('/'); + const pathParts = path.split('/'); + + if (patternParts.length !== pathParts.length) return null; + + const params = {}; + for (let i = 0; i < patternParts.length; i++) { + if (patternParts[i].startsWith(':')) { + params[patternParts[i].slice(1)] = pathParts[i]; + } else if (patternParts[i] !== pathParts[i]) { + return null; + } + } + return params; +} + +function getCurrentPath() { + return window.location.hash.slice(1) || '/'; +} + +function resolve() { + const path = getCurrentPath(); + const match = matchRoute(path); + + if (match) { + match.handler(match.params); + } else if (notFoundHandler) { + notFoundHandler(); + } +} + +export const router = { + on(pattern, handler) { + routes.push({ pattern, handler }); + }, + + notFound(handler) { + notFoundHandler = handler; + }, + + navigate(path) { + window.location.hash = path; + }, + + getCurrentPath, + + start() { + window.addEventListener('hashchange', resolve); + resolve(); + }, +}; diff --git a/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/store.js b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/store.js new file mode 100644 index 000000000..97425a0d0 --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/store.js @@ -0,0 +1,272 @@ +/** + * Application state backed by localStorage. + * + * Provides a reactive-ish store: call store.save() after mutations + * and re-render your current view. + */ + +const STORAGE_KEY = 'trackflow_data'; + +const DEFAULT_STATE = { + currentUser: null, + activities: [ + { id: 'a1', action: 'created_project', detail: 'Marketing Website', user: 'alice', timestamp: '2025-12-01T10:00:00Z' }, + { id: 'a2', action: 'added_task', detail: 'Design homepage mockup → Marketing Website', user: 'alice', timestamp: '2025-12-01T10:05:00Z' }, + { id: 'a3', action: 'created_project', detail: 'Mobile App v2', user: 'bob', timestamp: '2025-11-15T09:00:00Z' }, + { id: 'a4', action: 'completed_task', detail: 'Design homepage mockup', user: 'alice', timestamp: '2025-12-10T14:30:00Z' }, + { id: 'a5', action: 'completed_task', detail: 'Implement responsive nav', user: 'bob', timestamp: '2025-12-12T11:00:00Z' }, + { id: 'a6', action: 'assigned_task', detail: 'Write landing page copy → Carol Kim', user: 'alice', timestamp: '2025-12-03T15:00:00Z' }, + { id: 'a7', action: 'moved_task', detail: 'Write landing page copy → In Progress', user: 'carol', timestamp: '2025-12-05T09:00:00Z' }, + { id: 'a8', action: 'created_project', detail: 'API Migration', user: 'bob', timestamp: '2025-10-01T08:00:00Z' }, + { id: 'a9', action: 'completed_task', detail: 'Schema design', user: 'bob', timestamp: '2025-10-20T16:00:00Z' }, + { id: 'a10', action: 'completed_task', detail: 'Client migration', user: 'carol', timestamp: '2025-11-01T10:00:00Z' }, + ], + projects: [ + { + id: 'proj_1', + name: 'Marketing Website', + description: 'Redesign the company marketing site', + status: 'active', + createdAt: '2025-12-01T10:00:00Z', + tasks: [ + { id: 't1', title: 'Design homepage mockup', status: 'done', priority: 'high', assignee: 'alice', createdAt: '2025-12-01T10:00:00Z' }, + { id: 't2', title: 'Implement responsive nav', status: 'done', priority: 'medium', assignee: 'bob', createdAt: '2025-12-02T09:00:00Z' }, + { id: 't3', title: 'Write landing page copy', status: 'in_progress', priority: 'high', assignee: 'carol', createdAt: '2025-12-03T14:00:00Z' }, + { id: 't4', title: 'Set up analytics tracking', status: 'todo', priority: 'medium', assignee: null, createdAt: '2025-12-04T11:00:00Z' }, + { id: 't5', title: 'Performance audit', status: 'todo', priority: 'low', assignee: null, createdAt: '2025-12-05T08:00:00Z' }, + ], + }, + { + id: 'proj_2', + name: 'Mobile App v2', + description: 'Major feature update for the mobile application', + status: 'active', + createdAt: '2025-11-15T09:00:00Z', + tasks: [ + { id: 't6', title: 'Push notification system', status: 'in_progress', priority: 'high', assignee: 'alice', createdAt: '2025-11-15T09:00:00Z' }, + { id: 't7', title: 'Offline mode support', status: 'todo', priority: 'high', assignee: 'bob', createdAt: '2025-11-16T10:00:00Z' }, + { id: 't8', title: 'Dark mode theme', status: 'done', priority: 'medium', assignee: 'carol', createdAt: '2025-11-17T13:00:00Z' }, + { id: 't9', title: 'App store screenshots', status: 'todo', priority: 'low', assignee: null, createdAt: '2025-11-20T16:00:00Z' }, + ], + }, + { + id: 'proj_3', + name: 'API Migration', + description: 'Migrate REST API to GraphQL', + status: 'archived', + createdAt: '2025-10-01T08:00:00Z', + tasks: [ + { id: 't10', title: 'Schema design', status: 'done', priority: 'high', assignee: 'bob', createdAt: '2025-10-01T08:00:00Z' }, + { id: 't11', title: 'Resolver implementation', status: 'done', priority: 'high', assignee: 'alice', createdAt: '2025-10-05T09:00:00Z' }, + { id: 't12', title: 'Client migration', status: 'done', priority: 'medium', assignee: 'carol', createdAt: '2025-10-15T11:00:00Z' }, + ], + }, + ], + teamMembers: [ + { id: 'alice', name: 'Alice Chen', email: 'alice@trackflow.dev', role: 'admin', avatar: 'AC' }, + { id: 'bob', name: 'Bob Martinez', email: 'bob@trackflow.dev', role: 'member', avatar: 'BM' }, + { id: 'carol', name: 'Carol Kim', email: 'carol@trackflow.dev', role: 'member', avatar: 'CK' }, + ], + settings: { + theme: 'light', + emailNotifications: true, + weeklyDigest: true, + }, +}; + +function load() { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return structuredClone(DEFAULT_STATE); + const parsed = JSON.parse(raw); + // Backfill activities for existing localStorage data + if (!parsed.activities) parsed.activities = structuredClone(DEFAULT_STATE.activities); + return parsed; + } catch { + return structuredClone(DEFAULT_STATE); + } +} + +let state = load(); + +export const store = { + get state() { + return state; + }, + + save() { + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); + }, + + reset() { + state = structuredClone(DEFAULT_STATE); + this.save(); + }, + + // --- Activities --- + + logActivity(action, detail) { + const activity = { + id: `a_${Date.now()}`, + action, + detail, + user: state.currentUser?.id || 'system', + timestamp: new Date().toISOString(), + }; + state.activities.unshift(activity); + // Keep last 50 entries + if (state.activities.length > 50) state.activities.length = 50; + this.save(); + }, + + getActivities() { + return [...state.activities].sort( + (a, b) => new Date(b.timestamp) - new Date(a.timestamp), + ); + }, + + // --- Auth --- + + login(email) { + const member = state.teamMembers.find((m) => m.email === email); + if (!member) return false; + state.currentUser = member; + this.save(); + this.logActivity('logged_in', member.name); + return true; + }, + + logout() { + state.currentUser = null; + this.save(); + }, + + // --- Projects --- + + getProject(id) { + return state.projects.find((p) => p.id === id); + }, + + createProject(name, description) { + const project = { + id: `proj_${Date.now()}`, + name, + description, + status: 'active', + createdAt: new Date().toISOString(), + tasks: [], + }; + state.projects.push(project); + this.save(); + this.logActivity('created_project', name); + return project; + }, + + deleteProject(id) { + const project = this.getProject(id); + const name = project?.name || id; + state.projects = state.projects.filter((p) => p.id !== id); + this.save(); + this.logActivity('deleted_project', name); + }, + + // --- Tasks --- + + addTask(projectId, title, priority = 'medium') { + const project = this.getProject(projectId); + if (!project) return null; + + const task = { + id: `t_${Date.now()}`, + title, + status: 'todo', + priority, + assignee: null, + createdAt: new Date().toISOString(), + }; + project.tasks.push(task); + this.save(); + this.logActivity('added_task', `${title} → ${project.name}`); + return task; + }, + + updateTaskStatus(projectId, taskId, status) { + const project = this.getProject(projectId); + if (!project) return; + + const task = project.tasks.find((t) => t.id === taskId); + if (task) { + task.status = status; + this.save(); + const label = status === 'done' ? 'Completed' : status === 'in_progress' ? 'In Progress' : 'To Do'; + this.logActivity( + status === 'done' ? 'completed_task' : 'moved_task', + `${task.title} → ${label}`, + ); + } + }, + + deleteTask(projectId, taskId) { + const project = this.getProject(projectId); + if (!project) return; + + const task = project.tasks.find((t) => t.id === taskId); + project.tasks = project.tasks.filter((t) => t.id !== taskId); + this.save(); + if (task) this.logActivity('deleted_task', task.title); + }, + + assignTask(projectId, taskId, assigneeId) { + const project = this.getProject(projectId); + if (!project) return; + + const task = project.tasks.find((t) => t.id === taskId); + if (task) { + task.assignee = assigneeId; + this.save(); + const member = state.teamMembers.find((m) => m.id === assigneeId); + this.logActivity( + 'assigned_task', + `${task.title} → ${member?.name || 'Unassigned'}`, + ); + } + }, + + // --- Settings --- + + updateSettings(updates) { + Object.assign(state.settings, updates); + this.save(); + }, + + // --- Stats --- + + getStats() { + const activeProjects = state.projects.filter((p) => p.status === 'active'); + const allTasks = state.projects.flatMap((p) => p.tasks); + const doneTasks = allTasks.filter((t) => t.status === 'done'); + const inProgressTasks = allTasks.filter((t) => t.status === 'in_progress'); + const todoTasks = allTasks.filter((t) => t.status === 'todo'); + + return { + totalProjects: state.projects.length, + activeProjects: activeProjects.length, + totalTasks: allTasks.length, + doneTasks: doneTasks.length, + inProgressTasks: inProgressTasks.length, + todoTasks: todoTasks.length, + completionRate: allTasks.length > 0 + ? Math.round((doneTasks.length / allTasks.length) * 100) + : 0, + tasksByPriority: { + high: allTasks.filter((t) => t.priority === 'high').length, + medium: allTasks.filter((t) => t.priority === 'medium').length, + low: allTasks.filter((t) => t.priority === 'low').length, + }, + tasksByMember: state.teamMembers.map((m) => ({ + name: m.name, + count: allTasks.filter((t) => t.assignee === m.id).length, + })), + }; + }, +}; diff --git a/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/styles/app.css b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/styles/app.css new file mode 100644 index 000000000..4c450ce99 --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/styles/app.css @@ -0,0 +1,1082 @@ +/* =========================== + Variables + =========================== */ + +:root { + --color-primary: #4f46e5; + --color-primary-hover: #4338ca; + --color-danger: #ef4444; + --color-danger-hover: #dc2626; + --color-success: #22c55e; + --color-warning: #f59e0b; + --color-text: #1a1a2e; + --color-text-muted: #6b7280; + --color-bg: #f5f6fa; + --color-surface: #ffffff; + --color-border: #e5e7eb; + --sidebar-width: 240px; + --topbar-height: 56px; + --radius: 8px; + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); +} + +[data-theme='dark'] { + --color-text: #e5e7eb; + --color-text-muted: #9ca3af; + --color-bg: #0f172a; + --color-surface: #1e293b; + --color-border: #334155; +} + +/* =========================== + Layout + =========================== */ + +.app-layout { + display: flex; + min-height: 100vh; +} + +.main-area { + flex: 1; + margin-left: var(--sidebar-width); + display: flex; + flex-direction: column; +} + +.content { + flex: 1; + padding: 1.5rem 2rem; + max-width: 1200px; + width: 100%; +} + +/* =========================== + Sidebar + =========================== */ + +.sidebar { + position: fixed; + top: 0; + left: 0; + bottom: 0; + width: var(--sidebar-width); + background: var(--color-text); + color: #fff; + display: flex; + flex-direction: column; + z-index: 100; +} + +.sidebar-brand { + padding: 1.25rem 1.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.sidebar-brand h1 { + font-size: 1.25rem; + font-weight: 700; + letter-spacing: -0.02em; +} + +.sidebar-nav { + padding: 1rem 0; + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.nav-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.625rem 1.5rem; + color: rgba(255, 255, 255, 0.7); + font-size: 0.9rem; + transition: background 0.15s, color 0.15s; +} + +.nav-item:hover { + background: rgba(255, 255, 255, 0.08); + color: #fff; +} + +.nav-item.active { + background: rgba(255, 255, 255, 0.12); + color: #fff; + font-weight: 600; +} + +.nav-icon { + font-size: 1.1rem; + width: 1.25rem; + text-align: center; +} + +/* =========================== + Topbar + =========================== */ + +.topbar { + height: var(--topbar-height); + background: var(--color-surface); + border-bottom: 1px solid var(--color-border); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 2rem; +} + +.topbar-left { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.greeting { + font-size: 0.9rem; + color: var(--color-text-muted); +} + +.topbar-right { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.topbar-avatar { + font-size: 0.75rem; +} + +.btn-text { + color: var(--color-text-muted); + font-size: 0.85rem; + padding: 0.25rem 0.5rem; + border-radius: 4px; + transition: color 0.15s, background 0.15s; +} + +.btn-text:hover { + color: var(--color-danger); + background: rgba(239, 68, 68, 0.08); +} + +/* =========================== + Page Header + =========================== */ + +.page-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1.5rem; +} + +.page-header h2 { + font-size: 1.5rem; + font-weight: 700; +} + +.back-link { + font-size: 0.85rem; + color: var(--color-primary); + margin-bottom: 0.25rem; + display: inline-block; +} + +.back-link:hover { + text-decoration: underline; +} + +.text-muted { + color: var(--color-text-muted); + font-size: 0.9rem; +} + +/* =========================== + Buttons + =========================== */ + +.btn-primary { + background: var(--color-primary); + color: #fff; + padding: 0.5rem 1.25rem; + border-radius: var(--radius); + font-size: 0.875rem; + font-weight: 500; + transition: background 0.15s; +} + +.btn-primary:hover { + background: var(--color-primary-hover); +} + +.btn-danger { + background: var(--color-danger); + color: #fff; + padding: 0.5rem 1.25rem; + border-radius: var(--radius); + font-size: 0.875rem; + font-weight: 500; + transition: background 0.15s; +} + +.btn-danger:hover { + background: var(--color-danger-hover); +} + +.btn-icon { + width: 28px; + height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 4px; + color: var(--color-text-muted); + transition: background 0.15s, color 0.15s; +} + +.btn-icon:hover { + background: rgba(239, 68, 68, 0.1); + color: var(--color-danger); +} + +.btn-tiny { + font-size: 0.7rem; + padding: 0.15rem 0.35rem; + border-radius: 3px; + color: var(--color-text-muted); + transition: background 0.15s, color 0.15s; +} + +.btn-tiny:hover { + background: var(--color-primary); + color: #fff; +} + +/* =========================== + Badge + =========================== */ + +.badge { + display: inline-block; + padding: 0.15rem 0.5rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; + text-transform: capitalize; + background: #e5e7eb; + color: #374151; +} + +.badge-active { + background: #dcfce7; + color: #166534; +} + +.badge-archived { + background: #f3f4f6; + color: #6b7280; +} + +.badge-admin { + background: #ede9fe; + color: #5b21b6; +} + +.badge-member { + background: #dbeafe; + color: #1e40af; +} + +/* =========================== + Avatar + =========================== */ + +.avatar { + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 50%; + background: var(--color-primary); + color: #fff; + font-size: 0.8rem; + font-weight: 600; + flex-shrink: 0; +} + +.avatar-small { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 50%; + background: var(--color-primary); + color: #fff; + font-size: 0.6rem; + font-weight: 600; +} + +/* =========================== + Loading / Error + =========================== */ + +.loading { + text-align: center; + padding: 3rem; + color: var(--color-text-muted); + font-size: 0.9rem; +} + +.error-message { + text-align: center; + padding: 2rem; + color: var(--color-danger); + background: rgba(239, 68, 68, 0.05); + border: 1px solid rgba(239, 68, 68, 0.2); + border-radius: var(--radius); +} + +.error { + color: var(--color-danger); + font-size: 0.85rem; + margin-top: 0.5rem; +} + +/* =========================== + Login + =========================== */ + +.login-page { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); +} + +.login-card { + background: var(--color-surface); + padding: 2.5rem; + border-radius: 12px; + width: 100%; + max-width: 400px; + box-shadow: var(--shadow-md); +} + +.login-card h1 { + font-size: 1.75rem; + font-weight: 800; + color: var(--color-primary); + margin-bottom: 0.25rem; +} + +.login-subtitle { + color: var(--color-text-muted); + font-size: 0.9rem; + margin-bottom: 1.5rem; +} + +#login-form { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +#login-form label { + font-size: 0.85rem; + font-weight: 500; +} + +#login-form button { + background: var(--color-primary); + color: #fff; + padding: 0.625rem; + border-radius: var(--radius); + font-weight: 600; + margin-top: 0.5rem; + transition: background 0.15s; +} + +#login-form button:hover { + background: var(--color-primary-hover); +} + +#login-form button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.login-hint { + margin-top: 1.5rem; + padding-top: 1rem; + border-top: 1px solid var(--color-border); + font-size: 0.8rem; + color: var(--color-text-muted); +} + +.login-hint p { + margin-bottom: 0.5rem; + font-weight: 500; +} + +.login-hint li { + padding: 0.15rem 0; +} + +/* =========================== + Stats Grid (Dashboard) + =========================== */ + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 1rem; + margin-bottom: 2rem; +} + +.stat-card { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius); + padding: 1.25rem; + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.stat-value { + font-size: 1.75rem; + font-weight: 700; + color: var(--color-primary); +} + +.stat-label { + font-size: 0.8rem; + color: var(--color-text-muted); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +/* =========================== + Dashboard Sections + =========================== */ + +.dashboard-columns { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1.5rem; + margin-bottom: 2rem; +} + +.dashboard-section { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius); + padding: 1.25rem; + margin-bottom: 1.5rem; +} + +.dashboard-section h3 { + font-size: 1rem; + font-weight: 600; + margin-bottom: 1rem; +} + +/* =========================== + Progress Bars + =========================== */ + +.progress-bars { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.progress-row { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.progress-label { + font-size: 0.8rem; + color: var(--color-text-muted); + width: 80px; + flex-shrink: 0; +} + +.progress-track { + flex: 1; + height: 8px; + background: #e5e7eb; + border-radius: 4px; + overflow: hidden; +} + +.progress-track.small { + height: 6px; +} + +.progress-fill { + height: 100%; + border-radius: 4px; + transition: width 0.3s ease; +} + +.progress-fill.done { + background: var(--color-success); +} + +.progress-fill.in-progress { + background: var(--color-warning); +} + +.progress-fill.todo { + background: #94a3b8; +} + +.progress-count { + font-size: 0.8rem; + font-weight: 600; + color: var(--color-text-muted); + width: 24px; + text-align: right; +} + +/* =========================== + Team List (Dashboard) + =========================== */ + +.team-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.team-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0; + border-bottom: 1px solid var(--color-border); +} + +.team-row:last-child { + border-bottom: none; +} + +.team-name { + font-size: 0.875rem; + font-weight: 500; +} + +.team-count { + font-size: 0.8rem; + color: var(--color-text-muted); +} + +/* =========================== + Project Cards (Dashboard) + =========================== */ + +.project-cards { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 1rem; +} + +.project-card-link { + display: block; +} + +.project-card { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius); + padding: 1rem; + transition: border-color 0.15s, box-shadow 0.15s; +} + +.project-card:hover { + border-color: var(--color-primary); + box-shadow: var(--shadow-sm); +} + +.project-card h4 { + font-size: 0.95rem; + margin-bottom: 0.25rem; +} + +.project-card p { + font-size: 0.8rem; + color: var(--color-text-muted); + margin-bottom: 0.75rem; +} + +.project-card-meta { + display: flex; + gap: 1rem; + font-size: 0.75rem; + color: var(--color-text-muted); +} + +/* =========================== + Projects Table + =========================== */ + +.projects-table { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius); + overflow: hidden; +} + +.table-header { + display: grid; + grid-template-columns: 2fr 1fr 0.5fr 1.5fr 0.5fr; + padding: 0.75rem 1rem; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-text-muted); + border-bottom: 1px solid var(--color-border); + background: var(--color-bg); +} + +.table-row { + display: grid; + grid-template-columns: 2fr 1fr 0.5fr 1.5fr 0.5fr; + padding: 0.75rem 1rem; + align-items: center; + border-bottom: 1px solid var(--color-border); + transition: background 0.1s; +} + +.table-row:last-child { + border-bottom: none; +} + +.table-row:hover { + background: rgba(79, 70, 229, 0.02); +} + +.col-name a { + font-weight: 500; + color: var(--color-primary); +} + +.col-name a:hover { + text-decoration: underline; +} + +.col-name small { + display: block; + font-size: 0.8rem; + color: var(--color-text-muted); +} + +.col-tasks { + font-size: 0.875rem; + font-weight: 500; +} + +.col-progress { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.col-progress small { + font-size: 0.75rem; + color: var(--color-text-muted); + width: 30px; +} + +.col-actions { + text-align: right; +} + +/* =========================== + Kanban Board (Project Detail) + =========================== */ + +.board { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; +} + +.board-column { + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius); + padding: 1rem; + min-height: 200px; +} + +.board-column-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid var(--color-border); +} + +.board-column-header h3 { + font-size: 0.85rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.board-cards { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.task-card { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 6px; + padding: 0.75rem; + box-shadow: var(--shadow-sm); + transition: box-shadow 0.15s; +} + +.task-card:hover { + box-shadow: var(--shadow-md); +} + +.task-card-header { + display: flex; + align-items: flex-start; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.priority-dot { + width: 8px; + height: 8px; + border-radius: 50%; + margin-top: 0.35rem; + flex-shrink: 0; +} + +.priority-high { + background: var(--color-danger); +} + +.priority-medium { + background: var(--color-warning); +} + +.priority-low { + background: #94a3b8; +} + +.task-title { + font-size: 0.85rem; + font-weight: 500; + line-height: 1.3; +} + +.task-card-footer { + display: flex; + justify-content: space-between; + align-items: center; +} + +.task-actions { + display: flex; + gap: 0.25rem; +} + +.unassigned { + font-size: 0.7rem; + color: var(--color-text-muted); + font-style: italic; +} + +/* =========================== + Settings + =========================== */ + +.settings-sections { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.settings-section { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius); + padding: 1.25rem; +} + +.settings-section h3 { + font-size: 1rem; + font-weight: 600; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--color-border); +} + +.settings-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.625rem 0; +} + +.settings-row label { + font-size: 0.875rem; + font-weight: 500; +} + +.settings-row span { + font-size: 0.875rem; + color: var(--color-text-muted); +} + +.settings-row select { + width: auto; + min-width: 120px; +} + +.settings-row input[type='checkbox'] { + width: auto; + accent-color: var(--color-primary); +} + +/* =========================== + Team Members (Settings) + =========================== */ + +.team-members-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.team-member-row { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.5rem 0; + border-bottom: 1px solid var(--color-border); +} + +.team-member-row:last-child { + border-bottom: none; +} + +.team-member-info { + flex: 1; + display: flex; + flex-direction: column; +} + +.team-member-info strong { + font-size: 0.875rem; +} + +.team-member-info small { + font-size: 0.75rem; + color: var(--color-text-muted); +} + +/* =========================== + Modal + =========================== */ + +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.4); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.modal { + background: var(--color-surface); + border-radius: 12px; + width: 100%; + max-width: 440px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15); + overflow: hidden; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 1.25rem; + border-bottom: 1px solid var(--color-border); +} + +.modal-header h3 { + font-size: 1rem; + font-weight: 600; +} + +.modal-close { + font-size: 1.25rem; + color: var(--color-text-muted); + padding: 0.25rem; + line-height: 1; +} + +.modal-close:hover { + color: var(--color-danger); +} + +.modal-body { + padding: 1.25rem; +} + +.modal-body form { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.modal-body form label { + font-size: 0.85rem; + font-weight: 500; +} + +.modal-body form button[type='submit'] { + margin-top: 0.5rem; +} + +/* =========================== + Assign Modal + =========================== */ + +.assign-list { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.assign-option { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.625rem 0.75rem; + border-radius: 6px; + font-size: 0.875rem; + text-align: left; + width: 100%; + transition: background 0.1s; +} + +.assign-option:hover { + background: rgba(79, 70, 229, 0.06); +} + +/* =========================== + Charts + =========================== */ + +.chart-container { + position: relative; + height: 220px; +} + +/* =========================== + Activity Log + =========================== */ + +.activity-list { + display: flex; + flex-direction: column; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius); + overflow: hidden; +} + +.activity-list.compact { + border: none; + background: none; +} + +.activity-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--color-border); +} + +.activity-item:last-child { + border-bottom: none; +} + +.activity-icon { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.9rem; + flex-shrink: 0; +} + +.activity-body { + flex: 1; + font-size: 0.85rem; + line-height: 1.4; + min-width: 0; +} + +.activity-user { + font-weight: 600; +} + +.activity-action { + color: var(--color-text-muted); +} + +.activity-detail { + font-weight: 500; +} + +.activity-time { + font-size: 0.75rem; + color: var(--color-text-muted); + white-space: nowrap; + flex-shrink: 0; +} + +.section-header-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.section-header-row h3 { + margin-bottom: 0; +} + +.view-all-link { + font-size: 0.8rem; + color: var(--color-primary); +} + +.view-all-link:hover { + text-decoration: underline; +} diff --git a/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/styles/reset.css b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/styles/reset.css new file mode 100644 index 000000000..4fab21e51 --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/src/styles/reset.css @@ -0,0 +1,60 @@ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: 16px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, + Ubuntu, Cantarell, sans-serif; + line-height: 1.5; + color: #1a1a2e; + background: #f5f6fa; +} + +a { + color: inherit; + text-decoration: none; +} + +button { + cursor: pointer; + font: inherit; + border: none; + background: none; +} + +input, +select, +textarea { + font: inherit; + border: 1px solid #d1d5db; + border-radius: 6px; + padding: 0.5rem 0.75rem; + width: 100%; + outline: none; + transition: border-color 0.15s; +} + +input:focus, +select:focus, +textarea:focus { + border-color: #4f46e5; + box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1); +} + +ul { + list-style: none; +} + +img { + max-width: 100%; +} diff --git a/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/vite.config.js b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/vite.config.js new file mode 100644 index 000000000..b531c15dd --- /dev/null +++ b/apps/error-tracking/no-posthog-javascript-web/saas-dashboard/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + server: { + port: 3000, + }, +}); diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/.env.example b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/.env.example new file mode 100644 index 000000000..9f67031a4 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/.env.example @@ -0,0 +1,11 @@ +# This is an example of your .env file format, which pnpm db:setup will create. +# Note: this must be .env, not .env.local, without further configuration changes. +POSTGRES_URL=postgresql://*** +STRIPE_SECRET_KEY=sk_test_*** +STRIPE_WEBHOOK_SECRET=whsec_*** +BASE_URL=http://localhost:3000 +AUTH_SECRET=*** + +# Stripe Stub Mode (optional) +# Set to 'stub' to use mock Stripe implementation without real API keys +# STRIPE_MODE=stub \ No newline at end of file diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/LICENSE b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/LICENSE new file mode 100644 index 000000000..b3ddd8d1d --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Vercel + +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. diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/README.md b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/README.md new file mode 100644 index 000000000..093d91d2e --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/README.md @@ -0,0 +1,119 @@ +# Next.js SaaS Starter + +This is a starter template for building a SaaS application using **Next.js** with support for authentication, Stripe integration for payments, and a dashboard for logged-in users. + +**Demo: [https://next-saas-start.vercel.app/](https://next-saas-start.vercel.app/)** + +## Features + +- Marketing landing page (`/`) with animated Terminal element +- Pricing page (`/pricing`) which connects to Stripe Checkout +- Dashboard pages with CRUD operations on users/teams +- Basic RBAC with Owner and Member roles +- Subscription management with Stripe Customer Portal +- Email/password authentication with JWTs stored to cookies +- Global middleware to protect logged-in routes +- Local middleware to protect Server Actions or validate Zod schemas +- Activity logging system for any user events + +## Tech Stack + +- **Framework**: [Next.js](https://nextjs.org/) +- **Database**: [Postgres](https://www.postgresql.org/) +- **ORM**: [Drizzle](https://orm.drizzle.team/) +- **Payments**: [Stripe](https://stripe.com/) +- **UI Library**: [shadcn/ui](https://ui.shadcn.com/) + +## Getting Started + +```bash +git clone https://github.com/nextjs/saas-starter +cd saas-starter +pnpm install +``` + +## Running Locally + +[Install](https://docs.stripe.com/stripe-cli) and log in to your Stripe account: + +```bash +stripe login +``` + +Use the included setup script to create your `.env` file: + +```bash +pnpm db:setup +``` + +Run the database migrations and seed the database with a default user and team: + +```bash +pnpm db:migrate +pnpm db:seed +``` + +This will create the following user and team: + +- User: `test@test.com` +- Password: `admin123` + +You can also create new users through the `/sign-up` route. + +Finally, run the Next.js development server: + +```bash +pnpm dev +``` + +Open [http://localhost:3000](http://localhost:3000) in your browser to see the app in action. + +You can listen for Stripe webhooks locally through their CLI to handle subscription change events: + +```bash +stripe listen --forward-to localhost:3000/api/stripe/webhook +``` + +## Testing Payments + +To test Stripe payments, use the following test card details: + +- Card Number: `4242 4242 4242 4242` +- Expiration: Any future date +- CVC: Any 3-digit number + +## Going to Production + +When you're ready to deploy your SaaS application to production, follow these steps: + +### Set up a production Stripe webhook + +1. Go to the Stripe Dashboard and create a new webhook for your production environment. +2. Set the endpoint URL to your production API route (e.g., `https://yourdomain.com/api/stripe/webhook`). +3. Select the events you want to listen for (e.g., `checkout.session.completed`, `customer.subscription.updated`). + +### Deploy to Vercel + +1. Push your code to a GitHub repository. +2. Connect your repository to [Vercel](https://vercel.com/) and deploy it. +3. Follow the Vercel deployment process, which will guide you through setting up your project. + +### Add environment variables + +In your Vercel project settings (or during deployment), add all the necessary environment variables. Make sure to update the values for the production environment, including: + +1. `BASE_URL`: Set this to your production domain. +2. `STRIPE_SECRET_KEY`: Use your Stripe secret key for the production environment. +3. `STRIPE_WEBHOOK_SECRET`: Use the webhook secret from the production webhook you created in step 1. +4. `POSTGRES_URL`: Set this to your production database URL. +5. `AUTH_SECRET`: Set this to a random string. `openssl rand -base64 32` will generate one. + +## Other Templates + +While this template is intentionally minimal and to be used as a learning resource, there are other paid versions in the community which are more full-featured: + +- https://achromatic.dev +- https://shipfa.st +- https://makerkit.dev +- https://zerotoshipped.com +- https://turbostarter.dev diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/dashboard/activity/loading.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/dashboard/activity/loading.tsx new file mode 100644 index 000000000..965f34cb5 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/dashboard/activity/loading.tsx @@ -0,0 +1,17 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; + +export default function ActivityPageSkeleton() { + return ( +
+

+ Activity Log +

+ + + Recent Activity + + + +
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/dashboard/activity/page.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/dashboard/activity/page.tsx new file mode 100644 index 000000000..68ada9699 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/dashboard/activity/page.tsx @@ -0,0 +1,126 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { + Settings, + LogOut, + UserPlus, + Lock, + UserCog, + AlertCircle, + UserMinus, + Mail, + CheckCircle, + type LucideIcon, +} from 'lucide-react'; +import { ActivityType } from '@/lib/db/schema'; +import { getActivityLogs } from '@/lib/db/queries'; + +const iconMap: Record = { + [ActivityType.SIGN_UP]: UserPlus, + [ActivityType.SIGN_IN]: UserCog, + [ActivityType.SIGN_OUT]: LogOut, + [ActivityType.UPDATE_PASSWORD]: Lock, + [ActivityType.DELETE_ACCOUNT]: UserMinus, + [ActivityType.UPDATE_ACCOUNT]: Settings, + [ActivityType.CREATE_TEAM]: UserPlus, + [ActivityType.REMOVE_TEAM_MEMBER]: UserMinus, + [ActivityType.INVITE_TEAM_MEMBER]: Mail, + [ActivityType.ACCEPT_INVITATION]: CheckCircle, +}; + +function getRelativeTime(date: Date) { + const now = new Date(); + const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000); + + if (diffInSeconds < 60) return 'just now'; + if (diffInSeconds < 3600) + return `${Math.floor(diffInSeconds / 60)} minutes ago`; + if (diffInSeconds < 86400) + return `${Math.floor(diffInSeconds / 3600)} hours ago`; + if (diffInSeconds < 604800) + return `${Math.floor(diffInSeconds / 86400)} days ago`; + return date.toLocaleDateString(); +} + +function formatAction(action: ActivityType): string { + switch (action) { + case ActivityType.SIGN_UP: + return 'You signed up'; + case ActivityType.SIGN_IN: + return 'You signed in'; + case ActivityType.SIGN_OUT: + return 'You signed out'; + case ActivityType.UPDATE_PASSWORD: + return 'You changed your password'; + case ActivityType.DELETE_ACCOUNT: + return 'You deleted your account'; + case ActivityType.UPDATE_ACCOUNT: + return 'You updated your account'; + case ActivityType.CREATE_TEAM: + return 'You created a new team'; + case ActivityType.REMOVE_TEAM_MEMBER: + return 'You removed a team member'; + case ActivityType.INVITE_TEAM_MEMBER: + return 'You invited a team member'; + case ActivityType.ACCEPT_INVITATION: + return 'You accepted an invitation'; + default: + return 'Unknown action occurred'; + } +} + +export default async function ActivityPage() { + const logs = await getActivityLogs(); + + return ( +
+

+ Activity Log +

+ + + Recent Activity + + + {logs.length > 0 ? ( +
    + {logs.map((log) => { + const Icon = iconMap[log.action as ActivityType] || Settings; + const formattedAction = formatAction( + log.action as ActivityType + ); + + return ( +
  • +
    + +
    +
    +

    + {formattedAction} + {log.ipAddress && ` from IP ${log.ipAddress}`} +

    +

    + {getRelativeTime(new Date(log.timestamp))} +

    +
    +
  • + ); + })} +
+ ) : ( +
+ +

+ No activity yet +

+

+ When you perform actions like signing in or updating your + account, they'll appear here. +

+
+ )} +
+
+
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/dashboard/general/page.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/dashboard/general/page.tsx new file mode 100644 index 000000000..e3e0165aa --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/dashboard/general/page.tsx @@ -0,0 +1,121 @@ +'use client'; + +import { useActionState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Label } from '@/components/ui/label'; +import { Loader2 } from 'lucide-react'; +import { updateAccount } from '@/app/(login)/actions'; +import { User } from '@/lib/db/schema'; +import useSWR from 'swr'; +import { Suspense } from 'react'; + +const fetcher = (url: string) => fetch(url).then((res) => res.json()); + +type ActionState = { + name?: string; + error?: string; + success?: string; +}; + +type AccountFormProps = { + state: ActionState; + nameValue?: string; + emailValue?: string; +}; + +function AccountForm({ + state, + nameValue = '', + emailValue = '' +}: AccountFormProps) { + return ( + <> +
+ + +
+
+ + +
+ + ); +} + +function AccountFormWithData({ state }: { state: ActionState }) { + const { data: user } = useSWR('/api/user', fetcher); + return ( + + ); +} + +export default function GeneralPage() { + const [state, formAction, isPending] = useActionState( + updateAccount, + {} + ); + + return ( +
+

+ General Settings +

+ + + + Account Information + + +
+ }> + + + {state.error && ( +

{state.error}

+ )} + {state.success && ( +

{state.success}

+ )} + +
+
+
+
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/dashboard/layout.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/dashboard/layout.tsx new file mode 100644 index 000000000..be0396866 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/dashboard/layout.tsx @@ -0,0 +1,73 @@ +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { Button } from '@/components/ui/button'; +import { Users, Settings, Shield, Activity, Menu } from 'lucide-react'; + +export default function DashboardLayout({ + children +}: { + children: React.ReactNode; +}) { + const pathname = usePathname(); + const [isSidebarOpen, setIsSidebarOpen] = useState(false); + + const navItems = [ + { href: '/dashboard', icon: Users, label: 'Team' }, + { href: '/dashboard/general', icon: Settings, label: 'General' }, + { href: '/dashboard/activity', icon: Activity, label: 'Activity' }, + { href: '/dashboard/security', icon: Shield, label: 'Security' } + ]; + + return ( +
+ {/* Mobile header */} +
+
+ Settings +
+ +
+ +
+ {/* Sidebar */} + + + {/* Main content */} +
{children}
+
+
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/dashboard/page.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/dashboard/page.tsx new file mode 100644 index 000000000..60475a858 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/dashboard/page.tsx @@ -0,0 +1,287 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { + Card, + CardContent, + CardHeader, + CardTitle, + CardFooter +} from '@/components/ui/card'; +import { customerPortalAction } from '@/lib/payments/actions'; +import { useActionState } from 'react'; +import { TeamDataWithMembers, User } from '@/lib/db/schema'; +import { removeTeamMember, inviteTeamMember } from '@/app/(login)/actions'; +import useSWR from 'swr'; +import { Suspense } from 'react'; +import { Input } from '@/components/ui/input'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { Label } from '@/components/ui/label'; +import { Loader2, PlusCircle } from 'lucide-react'; + +type ActionState = { + error?: string; + success?: string; +}; + +const fetcher = (url: string) => fetch(url).then((res) => res.json()); + +function SubscriptionSkeleton() { + return ( + + + Team Subscription + + + ); +} + +function ManageSubscription() { + const { data: teamData } = useSWR('/api/team', fetcher); + + return ( + + + Team Subscription + + +
+
+
+

+ Current Plan: {teamData?.planName || 'Free'} +

+

+ {teamData?.subscriptionStatus === 'active' + ? 'Billed monthly' + : teamData?.subscriptionStatus === 'trialing' + ? 'Trial period' + : 'No active subscription'} +

+
+
+ +
+
+
+
+
+ ); +} + +function TeamMembersSkeleton() { + return ( + + + Team Members + + +
+
+
+
+
+
+
+
+
+
+
+ ); +} + +function TeamMembers() { + const { data: teamData } = useSWR('/api/team', fetcher); + const [removeState, removeAction, isRemovePending] = useActionState< + ActionState, + FormData + >(removeTeamMember, {}); + + const getUserDisplayName = (user: Pick) => { + return user.name || user.email || 'Unknown User'; + }; + + if (!teamData?.teamMembers?.length) { + return ( + + + Team Members + + +

No team members yet.

+
+
+ ); + } + + return ( + + + Team Members + + +
    + {teamData.teamMembers.map((member, index) => ( +
  • +
    + + {/* + This app doesn't save profile images, but here + is how you'd show them: + + + */} + + {getUserDisplayName(member.user) + .split(' ') + .map((n) => n[0]) + .join('')} + + +
    +

    + {getUserDisplayName(member.user)} +

    +

    + {member.role} +

    +
    +
    + {index > 1 ? ( +
    + + +
    + ) : null} +
  • + ))} +
+ {removeState?.error && ( +

{removeState.error}

+ )} +
+
+ ); +} + +function InviteTeamMemberSkeleton() { + return ( + + + Invite Team Member + + + ); +} + +function InviteTeamMember() { + const { data: user } = useSWR('/api/user', fetcher); + const isOwner = user?.role === 'owner'; + const [inviteState, inviteAction, isInvitePending] = useActionState< + ActionState, + FormData + >(inviteTeamMember, {}); + + return ( + + + Invite Team Member + + +
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+
+ {inviteState?.error && ( +

{inviteState.error}

+ )} + {inviteState?.success && ( +

{inviteState.success}

+ )} + +
+
+ {!isOwner && ( + +

+ You must be a team owner to invite new members. +

+
+ )} +
+ ); +} + +export default function SettingsPage() { + return ( +
+

Team Settings

+ }> + + + }> + + + }> + + +
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/dashboard/security/page.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/dashboard/security/page.tsx new file mode 100644 index 000000000..642b072da --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/dashboard/security/page.tsx @@ -0,0 +1,167 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Label } from '@/components/ui/label'; +import { Lock, Trash2, Loader2 } from 'lucide-react'; +import { useActionState } from 'react'; +import { updatePassword, deleteAccount } from '@/app/(login)/actions'; + +type PasswordState = { + currentPassword?: string; + newPassword?: string; + confirmPassword?: string; + error?: string; + success?: string; +}; + +type DeleteState = { + password?: string; + error?: string; + success?: string; +}; + +export default function SecurityPage() { + const [passwordState, passwordAction, isPasswordPending] = useActionState< + PasswordState, + FormData + >(updatePassword, {}); + + const [deleteState, deleteAction, isDeletePending] = useActionState< + DeleteState, + FormData + >(deleteAccount, {}); + + return ( +
+

+ Security Settings +

+ + + Password + + +
+
+ + +
+
+ + +
+
+ + +
+ {passwordState.error && ( +

{passwordState.error}

+ )} + {passwordState.success && ( +

{passwordState.success}

+ )} + +
+
+
+ + + + Delete Account + + +

+ Account deletion is non-reversable. Please proceed with caution. +

+
+
+ + +
+ {deleteState.error && ( +

{deleteState.error}

+ )} + +
+
+
+
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/layout.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/layout.tsx new file mode 100644 index 000000000..5e1d0b1b2 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/layout.tsx @@ -0,0 +1,106 @@ +'use client'; + +import Link from 'next/link'; +import { use, useState, Suspense } from 'react'; +import { Button } from '@/components/ui/button'; +import { CircleIcon, Home, LogOut } from 'lucide-react'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { signOut } from '@/app/(login)/actions'; +import { useRouter } from 'next/navigation'; +import { User } from '@/lib/db/schema'; +import useSWR, { mutate } from 'swr'; + +const fetcher = (url: string) => fetch(url).then((res) => res.json()); + +function UserMenu() { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const { data: user } = useSWR('/api/user', fetcher); + const router = useRouter(); + + async function handleSignOut() { + await signOut(); + mutate('/api/user'); + router.push('/'); + } + + if (!user) { + return ( + <> + + Pricing + + + + ); + } + + return ( + + + + + + {user.email + .split(' ') + .map((n) => n[0]) + .join('')} + + + + + + + + Dashboard + + +
+ +
+
+
+ ); +} + +function Header() { + return ( +
+
+ + + ACME + +
+ }> + + +
+
+
+ ); +} + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( +
+
+ {children} +
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/page.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/page.tsx new file mode 100644 index 000000000..07e3b487f --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/page.tsx @@ -0,0 +1,130 @@ +import { Button } from '@/components/ui/button'; +import { ArrowRight, CreditCard, Database } from 'lucide-react'; +import { Terminal } from './terminal'; + +export default function HomePage() { + return ( +
+
+
+
+
+

+ Build Your SaaS + Faster Than Ever +

+

+ Launch your SaaS product in record time with our powerful, + ready-to-use template. Packed with modern technologies and + essential integrations. +

+ +
+
+ +
+
+
+
+ +
+
+
+
+
+ + + +
+
+

+ Next.js and React +

+

+ Leverage the power of modern web technologies for optimal + performance and developer experience. +

+
+
+ +
+
+ +
+
+

+ Postgres and Drizzle ORM +

+

+ Robust database solution with an intuitive ORM for efficient + data management and scalability. +

+
+
+ +
+
+ +
+
+

+ Stripe Integration +

+

+ Seamless payment processing and subscription management with + industry-leading Stripe integration. +

+
+
+
+
+
+ +
+
+
+
+

+ Ready to launch your SaaS? +

+

+ Our template provides everything you need to get your SaaS up + and running quickly. Don't waste time on boilerplate - focus on + what makes your product unique. +

+
+ +
+
+
+
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/pricing/page.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/pricing/page.tsx new file mode 100644 index 000000000..234cbf5ed --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/pricing/page.tsx @@ -0,0 +1,94 @@ +import { checkoutAction } from '@/lib/payments/actions'; +import { Check } from 'lucide-react'; +import { getStripePrices, getStripeProducts } from '@/lib/payments/stripe'; +import { SubmitButton } from './submit-button'; + +// Prices are fresh for one hour max +export const revalidate = 3600; + +export default async function PricingPage() { + const [prices, products] = await Promise.all([ + getStripePrices(), + getStripeProducts(), + ]); + + const basePlan = products.find((product) => product.name === 'Base'); + const plusPlan = products.find((product) => product.name === 'Plus'); + + const basePrice = prices.find((price) => price.productId === basePlan?.id); + const plusPrice = prices.find((price) => price.productId === plusPlan?.id); + + return ( +
+
+ + +
+
+ ); +} + +function PricingCard({ + name, + price, + interval, + trialDays, + features, + priceId, +}: { + name: string; + price: number; + interval: string; + trialDays: number; + features: string[]; + priceId?: string; +}) { + return ( +
+

{name}

+

+ with {trialDays} day free trial +

+

+ ${price / 100}{' '} + + per user / {interval} + +

+
    + {features.map((feature, index) => ( +
  • + + {feature} +
  • + ))} +
+
+ + + +
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/pricing/submit-button.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/pricing/submit-button.tsx new file mode 100644 index 000000000..aa04ea9db --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/pricing/submit-button.tsx @@ -0,0 +1,30 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { ArrowRight, Loader2 } from 'lucide-react'; +import { useFormStatus } from 'react-dom'; + +export function SubmitButton() { + const { pending } = useFormStatus(); + + return ( + + ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/terminal.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/terminal.tsx new file mode 100644 index 000000000..a80112317 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(dashboard)/terminal.tsx @@ -0,0 +1,68 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Copy, Check } from 'lucide-react'; + +export function Terminal() { + const [terminalStep, setTerminalStep] = useState(0); + const [copied, setCopied] = useState(false); + const terminalSteps = [ + 'git clone https://github.com/nextjs/saas-starter', + 'pnpm install', + 'pnpm db:setup', + 'pnpm db:migrate', + 'pnpm db:seed', + 'pnpm dev 🎉', + ]; + + useEffect(() => { + const timer = setTimeout(() => { + setTerminalStep((prev) => + prev < terminalSteps.length - 1 ? prev + 1 : prev + ); + }, 500); + + return () => clearTimeout(timer); + }, [terminalStep]); + + const copyToClipboard = () => { + navigator.clipboard.writeText(terminalSteps.join('\n')); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+
+
+
+
+
+
+
+ +
+
+ {terminalSteps.map((step, index) => ( +
terminalStep ? 'opacity-0' : 'opacity-100'} transition-opacity duration-300`} + > + $ {step} +
+ ))} +
+
+
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(login)/actions.ts b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(login)/actions.ts new file mode 100644 index 000000000..532adc0ef --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(login)/actions.ts @@ -0,0 +1,459 @@ +'use server'; + +import { z } from 'zod'; +import { and, eq, sql } from 'drizzle-orm'; +import { db } from '@/lib/db/drizzle'; +import { + User, + users, + teams, + teamMembers, + activityLogs, + type NewUser, + type NewTeam, + type NewTeamMember, + type NewActivityLog, + ActivityType, + invitations +} from '@/lib/db/schema'; +import { comparePasswords, hashPassword, setSession } from '@/lib/auth/session'; +import { redirect } from 'next/navigation'; +import { cookies } from 'next/headers'; +import { createCheckoutSession } from '@/lib/payments/stripe'; +import { getUser, getUserWithTeam } from '@/lib/db/queries'; +import { + validatedAction, + validatedActionWithUser +} from '@/lib/auth/middleware'; + +async function logActivity( + teamId: number | null | undefined, + userId: number, + type: ActivityType, + ipAddress?: string +) { + if (teamId === null || teamId === undefined) { + return; + } + const newActivity: NewActivityLog = { + teamId, + userId, + action: type, + ipAddress: ipAddress || '' + }; + await db.insert(activityLogs).values(newActivity); +} + +const signInSchema = z.object({ + email: z.string().email().min(3).max(255), + password: z.string().min(8).max(100) +}); + +export const signIn = validatedAction(signInSchema, async (data, formData) => { + const { email, password } = data; + + const userWithTeam = await db + .select({ + user: users, + team: teams + }) + .from(users) + .leftJoin(teamMembers, eq(users.id, teamMembers.userId)) + .leftJoin(teams, eq(teamMembers.teamId, teams.id)) + .where(eq(users.email, email)) + .limit(1); + + if (userWithTeam.length === 0) { + return { + error: 'Invalid email or password. Please try again.', + email, + password + }; + } + + const { user: foundUser, team: foundTeam } = userWithTeam[0]; + + const isPasswordValid = await comparePasswords( + password, + foundUser.passwordHash + ); + + if (!isPasswordValid) { + return { + error: 'Invalid email or password. Please try again.', + email, + password + }; + } + + await Promise.all([ + setSession(foundUser), + logActivity(foundTeam?.id, foundUser.id, ActivityType.SIGN_IN) + ]); + + const redirectTo = formData.get('redirect') as string | null; + if (redirectTo === 'checkout') { + const priceId = formData.get('priceId') as string; + return createCheckoutSession({ team: foundTeam, priceId }); + } + + redirect('/dashboard'); +}); + +const signUpSchema = z.object({ + email: z.string().email(), + password: z.string().min(8), + inviteId: z.string().optional() +}); + +export const signUp = validatedAction(signUpSchema, async (data, formData) => { + const { email, password, inviteId } = data; + + const existingUser = await db + .select() + .from(users) + .where(eq(users.email, email)) + .limit(1); + + if (existingUser.length > 0) { + return { + error: 'Failed to create user. Please try again.', + email, + password + }; + } + + const passwordHash = await hashPassword(password); + + const newUser: NewUser = { + email, + passwordHash, + role: 'owner' // Default role, will be overridden if there's an invitation + }; + + const [createdUser] = await db.insert(users).values(newUser).returning(); + + if (!createdUser) { + return { + error: 'Failed to create user. Please try again.', + email, + password + }; + } + + let teamId: number; + let userRole: string; + let createdTeam: typeof teams.$inferSelect | null = null; + + if (inviteId) { + // Check if there's a valid invitation + const [invitation] = await db + .select() + .from(invitations) + .where( + and( + eq(invitations.id, parseInt(inviteId)), + eq(invitations.email, email), + eq(invitations.status, 'pending') + ) + ) + .limit(1); + + if (invitation) { + teamId = invitation.teamId; + userRole = invitation.role; + + await db + .update(invitations) + .set({ status: 'accepted' }) + .where(eq(invitations.id, invitation.id)); + + await logActivity(teamId, createdUser.id, ActivityType.ACCEPT_INVITATION); + + [createdTeam] = await db + .select() + .from(teams) + .where(eq(teams.id, teamId)) + .limit(1); + } else { + return { error: 'Invalid or expired invitation.', email, password }; + } + } else { + // Create a new team if there's no invitation + const newTeam: NewTeam = { + name: `${email}'s Team` + }; + + [createdTeam] = await db.insert(teams).values(newTeam).returning(); + + if (!createdTeam) { + return { + error: 'Failed to create team. Please try again.', + email, + password + }; + } + + teamId = createdTeam.id; + userRole = 'owner'; + + await logActivity(teamId, createdUser.id, ActivityType.CREATE_TEAM); + } + + const newTeamMember: NewTeamMember = { + userId: createdUser.id, + teamId: teamId, + role: userRole + }; + + await Promise.all([ + db.insert(teamMembers).values(newTeamMember), + logActivity(teamId, createdUser.id, ActivityType.SIGN_UP), + setSession(createdUser) + ]); + + const redirectTo = formData.get('redirect') as string | null; + if (redirectTo === 'checkout') { + const priceId = formData.get('priceId') as string; + return createCheckoutSession({ team: createdTeam, priceId }); + } + + redirect('/dashboard'); +}); + +export async function signOut() { + const user = (await getUser()) as User; + const userWithTeam = await getUserWithTeam(user.id); + await logActivity(userWithTeam?.teamId, user.id, ActivityType.SIGN_OUT); + (await cookies()).delete('session'); +} + +const updatePasswordSchema = z.object({ + currentPassword: z.string().min(8).max(100), + newPassword: z.string().min(8).max(100), + confirmPassword: z.string().min(8).max(100) +}); + +export const updatePassword = validatedActionWithUser( + updatePasswordSchema, + async (data, _, user) => { + const { currentPassword, newPassword, confirmPassword } = data; + + const isPasswordValid = await comparePasswords( + currentPassword, + user.passwordHash + ); + + if (!isPasswordValid) { + return { + currentPassword, + newPassword, + confirmPassword, + error: 'Current password is incorrect.' + }; + } + + if (currentPassword === newPassword) { + return { + currentPassword, + newPassword, + confirmPassword, + error: 'New password must be different from the current password.' + }; + } + + if (confirmPassword !== newPassword) { + return { + currentPassword, + newPassword, + confirmPassword, + error: 'New password and confirmation password do not match.' + }; + } + + const newPasswordHash = await hashPassword(newPassword); + const userWithTeam = await getUserWithTeam(user.id); + + await Promise.all([ + db + .update(users) + .set({ passwordHash: newPasswordHash }) + .where(eq(users.id, user.id)), + logActivity(userWithTeam?.teamId, user.id, ActivityType.UPDATE_PASSWORD) + ]); + + return { + success: 'Password updated successfully.' + }; + } +); + +const deleteAccountSchema = z.object({ + password: z.string().min(8).max(100) +}); + +export const deleteAccount = validatedActionWithUser( + deleteAccountSchema, + async (data, _, user) => { + const { password } = data; + + const isPasswordValid = await comparePasswords(password, user.passwordHash); + if (!isPasswordValid) { + return { + password, + error: 'Incorrect password. Account deletion failed.' + }; + } + + const userWithTeam = await getUserWithTeam(user.id); + + await logActivity( + userWithTeam?.teamId, + user.id, + ActivityType.DELETE_ACCOUNT + ); + + // Soft delete + await db + .update(users) + .set({ + deletedAt: sql`CURRENT_TIMESTAMP`, + email: sql`CONCAT(email, '-', id, '-deleted')` // Ensure email uniqueness + }) + .where(eq(users.id, user.id)); + + if (userWithTeam?.teamId) { + await db + .delete(teamMembers) + .where( + and( + eq(teamMembers.userId, user.id), + eq(teamMembers.teamId, userWithTeam.teamId) + ) + ); + } + + (await cookies()).delete('session'); + redirect('/sign-in'); + } +); + +const updateAccountSchema = z.object({ + name: z.string().min(1, 'Name is required').max(100), + email: z.string().email('Invalid email address') +}); + +export const updateAccount = validatedActionWithUser( + updateAccountSchema, + async (data, _, user) => { + const { name, email } = data; + const userWithTeam = await getUserWithTeam(user.id); + + await Promise.all([ + db.update(users).set({ name, email }).where(eq(users.id, user.id)), + logActivity(userWithTeam?.teamId, user.id, ActivityType.UPDATE_ACCOUNT) + ]); + + return { name, success: 'Account updated successfully.' }; + } +); + +const removeTeamMemberSchema = z.object({ + memberId: z.number() +}); + +export const removeTeamMember = validatedActionWithUser( + removeTeamMemberSchema, + async (data, _, user) => { + const { memberId } = data; + const userWithTeam = await getUserWithTeam(user.id); + + if (!userWithTeam?.teamId) { + return { error: 'User is not part of a team' }; + } + + await db + .delete(teamMembers) + .where( + and( + eq(teamMembers.id, memberId), + eq(teamMembers.teamId, userWithTeam.teamId) + ) + ); + + await logActivity( + userWithTeam.teamId, + user.id, + ActivityType.REMOVE_TEAM_MEMBER + ); + + return { success: 'Team member removed successfully' }; + } +); + +const inviteTeamMemberSchema = z.object({ + email: z.string().email('Invalid email address'), + role: z.enum(['member', 'owner']) +}); + +export const inviteTeamMember = validatedActionWithUser( + inviteTeamMemberSchema, + async (data, _, user) => { + const { email, role } = data; + const userWithTeam = await getUserWithTeam(user.id); + + if (!userWithTeam?.teamId) { + return { error: 'User is not part of a team' }; + } + + const existingMember = await db + .select() + .from(users) + .leftJoin(teamMembers, eq(users.id, teamMembers.userId)) + .where( + and(eq(users.email, email), eq(teamMembers.teamId, userWithTeam.teamId)) + ) + .limit(1); + + if (existingMember.length > 0) { + return { error: 'User is already a member of this team' }; + } + + // Check if there's an existing invitation + const existingInvitation = await db + .select() + .from(invitations) + .where( + and( + eq(invitations.email, email), + eq(invitations.teamId, userWithTeam.teamId), + eq(invitations.status, 'pending') + ) + ) + .limit(1); + + if (existingInvitation.length > 0) { + return { error: 'An invitation has already been sent to this email' }; + } + + // Create a new invitation + await db.insert(invitations).values({ + teamId: userWithTeam.teamId, + email, + role, + invitedBy: user.id, + status: 'pending' + }); + + await logActivity( + userWithTeam.teamId, + user.id, + ActivityType.INVITE_TEAM_MEMBER + ); + + // TODO: Send invitation email and include ?inviteId={id} to sign-up URL + // await sendInvitationEmail(email, userWithTeam.team.name, role) + + return { success: 'Invitation sent successfully' }; + } +); diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(login)/login.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(login)/login.tsx new file mode 100644 index 000000000..86c7e280d --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(login)/login.tsx @@ -0,0 +1,142 @@ +'use client'; + +import Link from 'next/link'; +import { useActionState } from 'react'; +import { useSearchParams } from 'next/navigation'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { CircleIcon, Loader2 } from 'lucide-react'; +import { signIn, signUp } from './actions'; +import { ActionState } from '@/lib/auth/middleware'; + +export function Login({ mode = 'signin' }: { mode?: 'signin' | 'signup' }) { + const searchParams = useSearchParams(); + const redirect = searchParams.get('redirect'); + const priceId = searchParams.get('priceId'); + const inviteId = searchParams.get('inviteId'); + const [state, formAction, pending] = useActionState( + mode === 'signin' ? signIn : signUp, + { error: '' } + ); + + return ( +
+
+
+ +
+

+ {mode === 'signin' + ? 'Sign in to your account' + : 'Create your account'} +

+
+ +
+
+ + + +
+ +
+ +
+
+ +
+ +
+ +
+
+ + {state?.error && ( +
{state.error}
+ )} + +
+ +
+
+ +
+
+
+
+
+
+ + {mode === 'signin' + ? 'New to our platform?' + : 'Already have an account?'} + +
+
+ +
+ + {mode === 'signin' + ? 'Create an account' + : 'Sign in to existing account'} + +
+
+
+
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(login)/sign-in/page.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(login)/sign-in/page.tsx new file mode 100644 index 000000000..ed47e387e --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(login)/sign-in/page.tsx @@ -0,0 +1,10 @@ +import { Suspense } from 'react'; +import { Login } from '../login'; + +export default function SignInPage() { + return ( + + + + ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(login)/sign-up/page.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(login)/sign-up/page.tsx new file mode 100644 index 000000000..10ff615db --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/(login)/sign-up/page.tsx @@ -0,0 +1,10 @@ +import { Suspense } from 'react'; +import { Login } from '../login'; + +export default function SignUpPage() { + return ( + + + + ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/api/stripe/checkout/route.ts b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/api/stripe/checkout/route.ts new file mode 100644 index 000000000..d1eba26fd --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/api/stripe/checkout/route.ts @@ -0,0 +1,97 @@ +import { eq } from 'drizzle-orm'; +import { db } from '@/lib/db/drizzle'; +import { users, teams, teamMembers } from '@/lib/db/schema'; +import { setSession } from '@/lib/auth/session'; +import { NextRequest, NextResponse } from 'next/server'; +import { stripe } from '@/lib/payments/stripe'; +import Stripe from 'stripe'; + +export async function GET(request: NextRequest) { + const searchParams = request.nextUrl.searchParams; + const sessionId = searchParams.get('session_id'); + + if (!sessionId) { + return NextResponse.redirect(new URL('/pricing', request.url)); + } + + try { + const session = await stripe.checkout.sessions.retrieve(sessionId, { + expand: ['customer', 'subscription'], + }); + + if (!session.customer || typeof session.customer === 'string') { + throw new Error('Invalid customer data from Stripe.'); + } + + const customerId = session.customer.id; + const subscriptionId = + typeof session.subscription === 'string' + ? session.subscription + : session.subscription?.id; + + if (!subscriptionId) { + throw new Error('No subscription found for this session.'); + } + + const subscription = await stripe.subscriptions.retrieve(subscriptionId, { + expand: ['items.data.price.product'], + }); + + const plan = subscription.items.data[0]?.price; + + if (!plan) { + throw new Error('No plan found for this subscription.'); + } + + const productId = (plan.product as Stripe.Product).id; + + if (!productId) { + throw new Error('No product ID found for this subscription.'); + } + + const userId = session.client_reference_id; + if (!userId) { + throw new Error("No user ID found in session's client_reference_id."); + } + + const user = await db + .select() + .from(users) + .where(eq(users.id, Number(userId))) + .limit(1); + + if (user.length === 0) { + throw new Error('User not found in database.'); + } + + const userTeam = await db + .select({ + teamId: teamMembers.teamId, + }) + .from(teamMembers) + .where(eq(teamMembers.userId, user[0].id)) + .limit(1); + + if (userTeam.length === 0) { + throw new Error('User is not associated with any team.'); + } + + await db + .update(teams) + .set({ + stripeCustomerId: customerId, + stripeSubscriptionId: subscriptionId, + stripeProductId: productId, + planName: (plan.product as Stripe.Product).name, + subscriptionStatus: subscription.status, + updatedAt: new Date(), + }) + .where(eq(teams.id, userTeam[0].teamId)); + + await setSession(user[0]); + return NextResponse.redirect(new URL('/dashboard', request.url)); + } catch (error) { + console.error('Error handling successful checkout:', error); + return NextResponse.redirect(new URL('/error', request.url)); + } +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/api/stripe/webhook/route.ts b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/api/stripe/webhook/route.ts new file mode 100644 index 000000000..446066dc9 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/api/stripe/webhook/route.ts @@ -0,0 +1,35 @@ +import Stripe from 'stripe'; +import { handleSubscriptionChange, stripe } from '@/lib/payments/stripe'; +import { NextRequest, NextResponse } from 'next/server'; + +// Use a dummy webhook secret for stub mode +const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET || 'whsec_stub_secret'; + +export async function POST(request: NextRequest) { + const payload = await request.text(); + const signature = request.headers.get('stripe-signature') as string; + + let event: Stripe.Event; + + try { + event = stripe.webhooks.constructEvent(payload, signature, webhookSecret); + } catch (err) { + console.error('Webhook signature verification failed.', err); + return NextResponse.json( + { error: 'Webhook signature verification failed.' }, + { status: 400 } + ); + } + + switch (event.type) { + case 'customer.subscription.updated': + case 'customer.subscription.deleted': + const subscription = event.data.object as Stripe.Subscription; + await handleSubscriptionChange(subscription); + break; + default: + console.log(`Unhandled event type ${event.type}`); + } + + return NextResponse.json({ received: true }); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/api/team/route.ts b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/api/team/route.ts new file mode 100644 index 000000000..0659e7f8b --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/api/team/route.ts @@ -0,0 +1,6 @@ +import { getTeamForUser } from '@/lib/db/queries'; + +export async function GET() { + const team = await getTeamForUser(); + return Response.json(team); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/api/user/route.ts b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/api/user/route.ts new file mode 100644 index 000000000..6c3dc014c --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/api/user/route.ts @@ -0,0 +1,6 @@ +import { getUser } from '@/lib/db/queries'; + +export async function GET() { + const user = await getUser(); + return Response.json(user); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/favicon.ico b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/favicon.ico new file mode 100644 index 000000000..718d6fea4 Binary files /dev/null and b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/favicon.ico differ diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/globals.css b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/globals.css new file mode 100644 index 000000000..794a380a2 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/globals.css @@ -0,0 +1,275 @@ +@import "tailwindcss"; +/* + ---break--- +*/ +@custom-variant dark (&:is(.dark *)); + +@import "tw-animate-css"; + +@variant dark (&:is(.dark *)); + +@theme { + --color-background: hsl(var(--background)); + --color-foreground: hsl(var(--foreground)); + + --color-card: hsl(var(--card)); + --color-card-foreground: hsl(var(--card-foreground)); + + --color-popover: hsl(var(--popover)); + --color-popover-foreground: hsl(var(--popover-foreground)); + + --color-primary: hsl(var(--primary)); + --color-primary-foreground: hsl(var(--primary-foreground)); + + --color-secondary: hsl(var(--secondary)); + --color-secondary-foreground: hsl(var(--secondary-foreground)); + + --color-muted: hsl(var(--muted)); + --color-muted-foreground: hsl(var(--muted-foreground)); + + --color-accent: hsl(var(--accent)); + --color-accent-foreground: hsl(var(--accent-foreground)); + + --color-destructive: hsl(var(--destructive)); + --color-destructive-foreground: hsl(var(--destructive-foreground)); + + --color-border: hsl(var(--border)); + --color-input: hsl(var(--input)); + --color-ring: hsl(var(--ring)); + + --color-chart-1: hsl(var(--chart-1)); + --color-chart-2: hsl(var(--chart-2)); + --color-chart-3: hsl(var(--chart-3)); + --color-chart-4: hsl(var(--chart-4)); + --color-chart-5: hsl(var(--chart-5)); + + --color-sidebar: hsl(var(--sidebar-background)); + --color-sidebar-foreground: hsl(var(--sidebar-foreground)); + --color-sidebar-primary: hsl(var(--sidebar-primary)); + --color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground)); + --color-sidebar-accent: hsl(var(--sidebar-accent)); + --color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground)); + --color-sidebar-border: hsl(var(--sidebar-border)); + --color-sidebar-ring: hsl(var(--sidebar-ring)); + + --radius-lg: var(--radius); + --radius-md: calc(var(--radius) - 2px); + --radius-sm: calc(var(--radius) - 4px); +} + +/* + The default border color has changed to `currentColor` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. +*/ +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentColor); + } +} + +@layer utilities { + body { + font-family: "Manrope", Arial, Helvetica, sans-serif; + } +} + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} + +/* + ---break--- +*/ + +:root { + --background: hsl(0 0% 100%); + --foreground: hsl(240 10% 3.9%); + --card: hsl(0 0% 100%); + --card-foreground: hsl(240 10% 3.9%); + --popover: hsl(0 0% 100%); + --popover-foreground: hsl(240 10% 3.9%); + --primary: hsl(240 5.9% 10%); + --primary-foreground: hsl(0 0% 98%); + --secondary: hsl(240 4.8% 95.9%); + --secondary-foreground: hsl(240 5.9% 10%); + --muted: hsl(240 4.8% 95.9%); + --muted-foreground: hsl(240 3.8% 46.1%); + --accent: hsl(240 4.8% 95.9%); + --accent-foreground: hsl(240 5.9% 10%); + --destructive: hsl(0 84.2% 60.2%); + --destructive-foreground: hsl(0 0% 98%); + --border: hsl(240 5.9% 90%); + --input: hsl(240 5.9% 90%); + --ring: hsl(240 10% 3.9%); + --chart-1: hsl(12 76% 61%); + --chart-2: hsl(173 58% 39%); + --chart-3: hsl(197 37% 24%); + --chart-4: hsl(43 74% 66%); + --chart-5: hsl(27 87% 67%); + --radius: 0.6rem; +} + +/* + ---break--- +*/ + +.dark { + --background: hsl(240 10% 3.9%); + --foreground: hsl(0 0% 98%); + --card: hsl(240 10% 3.9%); + --card-foreground: hsl(0 0% 98%); + --popover: hsl(240 10% 3.9%); + --popover-foreground: hsl(0 0% 98%); + --primary: hsl(0 0% 98%); + --primary-foreground: hsl(240 5.9% 10%); + --secondary: hsl(240 3.7% 15.9%); + --secondary-foreground: hsl(0 0% 98%); + --muted: hsl(240 3.7% 15.9%); + --muted-foreground: hsl(240 5% 64.9%); + --accent: hsl(240 3.7% 15.9%); + --accent-foreground: hsl(0 0% 98%); + --destructive: hsl(0 62.8% 30.6%); + --destructive-foreground: hsl(0 0% 98%); + --border: hsl(240 3.7% 15.9%); + --input: hsl(240 3.7% 15.9%); + --ring: hsl(240 4.9% 83.9%); + --chart-1: hsl(220 70% 50%); + --chart-2: hsl(160 60% 45%); + --chart-3: hsl(30 80% 55%); + --chart-4: hsl(280 65% 60%); + --chart-5: hsl(340 75% 55%); +} + +/* + ---break--- +*/ + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); +} + +/* + ---break--- +*/ + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/layout.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/layout.tsx new file mode 100644 index 000000000..9c2596780 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/layout.tsx @@ -0,0 +1,44 @@ +import './globals.css'; +import type { Metadata, Viewport } from 'next'; +import { Manrope } from 'next/font/google'; +import { getUser, getTeamForUser } from '@/lib/db/queries'; +import { SWRConfig } from 'swr'; + +export const metadata: Metadata = { + title: 'Next.js SaaS Starter', + description: 'Get started quickly with Next.js, Postgres, and Stripe.' +}; + +export const viewport: Viewport = { + maximumScale: 1 +}; + +const manrope = Manrope({ subsets: ['latin'] }); + +export default function RootLayout({ + children +}: { + children: React.ReactNode; +}) { + return ( + + + + {children} + + + + ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/not-found.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/not-found.tsx new file mode 100644 index 000000000..7aad74b69 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/app/not-found.tsx @@ -0,0 +1,27 @@ +import Link from 'next/link'; +import { CircleIcon } from 'lucide-react'; + +export default function NotFound() { + return ( +
+
+
+ +
+

+ Page Not Found +

+

+ The page you are looking for might have been removed, had its name + changed, or is temporarily unavailable. +

+ + Back to Home + +
+
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/components.json b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/components.json new file mode 100644 index 000000000..5a3c7506d --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/components/ui/avatar.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/components/ui/avatar.tsx new file mode 100644 index 000000000..852cb27a6 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/components/ui/avatar.tsx @@ -0,0 +1,53 @@ +"use client"; + +import * as React from "react"; +import { Avatar as AvatarPrimitive } from "radix-ui";; + +import { cn } from "@/lib/utils"; + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/components/ui/button.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/components/ui/button.tsx new file mode 100644 index 000000000..82cad4b47 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/components/ui/button.tsx @@ -0,0 +1,59 @@ +import * as React from "react"; +import { Slot as SlotPrimitive } from "radix-ui";; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline" + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9" + } + }, + defaultVariants: { + variant: "default", + size: "default" + } + } +); + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean; + }) { + const Comp = asChild ? SlotPrimitive.Slot : "button"; + + return ( + + ); +} + +export { Button, buttonVariants }; diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/components/ui/card.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/components/ui/card.tsx new file mode 100644 index 000000000..4733d5844 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent +}; diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/components/ui/dropdown-menu.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/components/ui/dropdown-menu.tsx new file mode 100644 index 000000000..f84f00586 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/components/ui/dropdown-menu.tsx @@ -0,0 +1,257 @@ +"use client"; + +import * as React from "react"; +import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";; +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean; + variant?: "default" | "destructive"; +}) { + return ( + + ); +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + ); +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ); +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + {children} + + + ); +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent +}; diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/components/ui/input.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/components/ui/input.tsx new file mode 100644 index 000000000..0316cc455 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ); +} + +export { Input }; diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/components/ui/label.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/components/ui/label.tsx new file mode 100644 index 000000000..387e860dd --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/components/ui/label.tsx @@ -0,0 +1,24 @@ +"use client"; + +import * as React from "react"; +import { Label as LabelPrimitive } from "radix-ui";; + +import { cn } from "@/lib/utils"; + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Label }; diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/components/ui/radio-group.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/components/ui/radio-group.tsx new file mode 100644 index 000000000..cdf5e9dd5 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/components/ui/radio-group.tsx @@ -0,0 +1,45 @@ +"use client"; + +import * as React from "react"; +import { RadioGroup as RadioGroupPrimitive } from "radix-ui";; +import { CircleIcon } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +function RadioGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function RadioGroupItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ); +} + +export { RadioGroup, RadioGroupItem }; diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/docker-compose.yml b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/docker-compose.yml new file mode 100644 index 000000000..017d66652 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/docker-compose.yml @@ -0,0 +1,16 @@ + +services: + postgres: + image: postgres:16.4-alpine + container_name: next15_app_router_saas_starter_postgres + environment: + POSTGRES_DB: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "54322:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + +volumes: + postgres_data: diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/drizzle.config.ts b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/drizzle.config.ts new file mode 100644 index 000000000..6f6cc8873 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/drizzle.config.ts @@ -0,0 +1,10 @@ +import type { Config } from 'drizzle-kit'; + +export default { + schema: './lib/db/schema.ts', + out: './lib/db/migrations', + dialect: 'postgresql', + dbCredentials: { + url: process.env.POSTGRES_URL!, + }, +} satisfies Config; diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/auth/middleware.ts b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/auth/middleware.ts new file mode 100644 index 000000000..a17f25e0d --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/auth/middleware.ts @@ -0,0 +1,75 @@ +import { z } from 'zod'; +import { TeamDataWithMembers, User } from '@/lib/db/schema'; +import { getTeamForUser, getUser } from '@/lib/db/queries'; +import { redirect } from 'next/navigation'; + +export type ActionState = { + error?: string; + success?: string; + [key: string]: any; // This allows for additional properties +}; + +type ValidatedActionFunction, T> = ( + data: z.infer, + formData: FormData +) => Promise; + +export function validatedAction, T>( + schema: S, + action: ValidatedActionFunction +) { + return async (prevState: ActionState, formData: FormData) => { + const result = schema.safeParse(Object.fromEntries(formData)); + if (!result.success) { + return { error: result.error.errors[0].message }; + } + + return action(result.data, formData); + }; +} + +type ValidatedActionWithUserFunction, T> = ( + data: z.infer, + formData: FormData, + user: User +) => Promise; + +export function validatedActionWithUser, T>( + schema: S, + action: ValidatedActionWithUserFunction +) { + return async (prevState: ActionState, formData: FormData) => { + const user = await getUser(); + if (!user) { + throw new Error('User is not authenticated'); + } + + const result = schema.safeParse(Object.fromEntries(formData)); + if (!result.success) { + return { error: result.error.errors[0].message }; + } + + return action(result.data, formData, user); + }; +} + +type ActionWithTeamFunction = ( + formData: FormData, + team: TeamDataWithMembers +) => Promise; + +export function withTeam(action: ActionWithTeamFunction) { + return async (formData: FormData): Promise => { + const user = await getUser(); + if (!user) { + redirect('/sign-in'); + } + + const team = await getTeamForUser(); + if (!team) { + throw new Error('Team not found'); + } + + return action(formData, team); + }; +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/auth/session.ts b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/auth/session.ts new file mode 100644 index 000000000..0c474c70a --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/auth/session.ts @@ -0,0 +1,59 @@ +import { compare, hash } from 'bcryptjs'; +import { SignJWT, jwtVerify } from 'jose'; +import { cookies } from 'next/headers'; +import { NewUser } from '@/lib/db/schema'; + +const key = new TextEncoder().encode(process.env.AUTH_SECRET); +const SALT_ROUNDS = 10; + +export async function hashPassword(password: string) { + return hash(password, SALT_ROUNDS); +} + +export async function comparePasswords( + plainTextPassword: string, + hashedPassword: string +) { + return compare(plainTextPassword, hashedPassword); +} + +type SessionData = { + user: { id: number }; + expires: string; +}; + +export async function signToken(payload: SessionData) { + return await new SignJWT(payload) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('1 day from now') + .sign(key); +} + +export async function verifyToken(input: string) { + const { payload } = await jwtVerify(input, key, { + algorithms: ['HS256'], + }); + return payload as SessionData; +} + +export async function getSession() { + const session = (await cookies()).get('session')?.value; + if (!session) return null; + return await verifyToken(session); +} + +export async function setSession(user: NewUser) { + const expiresInOneDay = new Date(Date.now() + 24 * 60 * 60 * 1000); + const session: SessionData = { + user: { id: user.id! }, + expires: expiresInOneDay.toISOString(), + }; + const encryptedSession = await signToken(session); + (await cookies()).set('session', encryptedSession, { + expires: expiresInOneDay, + httpOnly: true, + secure: true, + sameSite: 'lax', + }); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/db/drizzle.ts b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/db/drizzle.ts new file mode 100644 index 000000000..7dc4a1b4f --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/db/drizzle.ts @@ -0,0 +1,13 @@ +import { drizzle } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; +import * as schema from './schema'; +import dotenv from 'dotenv'; + +dotenv.config(); + +if (!process.env.POSTGRES_URL) { + throw new Error('POSTGRES_URL environment variable is not set'); +} + +export const client = postgres(process.env.POSTGRES_URL); +export const db = drizzle(client, { schema }); diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/db/migrations/0000_soft_the_anarchist.sql b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/db/migrations/0000_soft_the_anarchist.sql new file mode 100644 index 000000000..b3dae0509 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/db/migrations/0000_soft_the_anarchist.sql @@ -0,0 +1,88 @@ +CREATE TABLE IF NOT EXISTS "activity_logs" ( + "id" serial PRIMARY KEY NOT NULL, + "team_id" integer NOT NULL, + "user_id" integer, + "action" text NOT NULL, + "timestamp" timestamp DEFAULT now() NOT NULL, + "ip_address" varchar(45) +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "invitations" ( + "id" serial PRIMARY KEY NOT NULL, + "team_id" integer NOT NULL, + "email" varchar(255) NOT NULL, + "role" varchar(50) NOT NULL, + "invited_by" integer NOT NULL, + "invited_at" timestamp DEFAULT now() NOT NULL, + "status" varchar(20) DEFAULT 'pending' NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "team_members" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" integer NOT NULL, + "team_id" integer NOT NULL, + "role" varchar(50) NOT NULL, + "joined_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "teams" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar(100) NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + "stripe_customer_id" text, + "stripe_subscription_id" text, + "stripe_product_id" text, + "plan_name" varchar(50), + "subscription_status" varchar(20), + CONSTRAINT "teams_stripe_customer_id_unique" UNIQUE("stripe_customer_id"), + CONSTRAINT "teams_stripe_subscription_id_unique" UNIQUE("stripe_subscription_id") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "users" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar(100), + "email" varchar(255) NOT NULL, + "password_hash" text NOT NULL, + "role" varchar(20) DEFAULT 'member' NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + "deleted_at" timestamp, + CONSTRAINT "users_email_unique" UNIQUE("email") +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "activity_logs" ADD CONSTRAINT "activity_logs_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "activity_logs" ADD CONSTRAINT "activity_logs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "invitations" ADD CONSTRAINT "invitations_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "invitations" ADD CONSTRAINT "invitations_invited_by_users_id_fk" FOREIGN KEY ("invited_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "team_members" ADD CONSTRAINT "team_members_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "team_members" ADD CONSTRAINT "team_members_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/db/migrations/meta/0000_snapshot.json b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/db/migrations/meta/0000_snapshot.json new file mode 100644 index 000000000..622eb9779 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/db/migrations/meta/0000_snapshot.json @@ -0,0 +1,389 @@ +{ + "id": "261fd993-fb2c-43e7-89d6-cd58786c5f58", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.activity_logs": { + "name": "activity_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_address": { + "name": "ip_address", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "activity_logs_team_id_teams_id_fk": { + "name": "activity_logs_team_id_teams_id_fk", + "tableFrom": "activity_logs", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_logs_user_id_users_id_fk": { + "name": "activity_logs_user_id_users_id_fk", + "tableFrom": "activity_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.invitations": { + "name": "invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "invited_at": { + "name": "invited_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + } + }, + "indexes": {}, + "foreignKeys": { + "invitations_team_id_teams_id_fk": { + "name": "invitations_team_id_teams_id_fk", + "tableFrom": "invitations", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "invitations_invited_by_users_id_fk": { + "name": "invitations_invited_by_users_id_fk", + "tableFrom": "invitations", + "tableTo": "users", + "columnsFrom": [ + "invited_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.team_members": { + "name": "team_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_members_user_id_users_id_fk": { + "name": "team_members_user_id_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "team_members_team_id_teams_id_fk": { + "name": "team_members_team_id_teams_id_fk", + "tableFrom": "team_members", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_product_id": { + "name": "stripe_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_name": { + "name": "plan_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "subscription_status": { + "name": "subscription_status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "teams_stripe_customer_id_unique": { + "name": "teams_stripe_customer_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_customer_id" + ] + }, + "teams_stripe_subscription_id_unique": { + "name": "teams_stripe_subscription_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_subscription_id" + ] + } + } + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + } + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/db/migrations/meta/_journal.json b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/db/migrations/meta/_journal.json new file mode 100644 index 000000000..fd4447417 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/db/migrations/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1726443359662, + "tag": "0000_soft_the_anarchist", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/db/queries.ts b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/db/queries.ts new file mode 100644 index 000000000..1bcdf644b --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/db/queries.ts @@ -0,0 +1,130 @@ +import { desc, and, eq, isNull } from 'drizzle-orm'; +import { db } from './drizzle'; +import { activityLogs, teamMembers, teams, users } from './schema'; +import { cookies } from 'next/headers'; +import { verifyToken } from '@/lib/auth/session'; + +export async function getUser() { + const sessionCookie = (await cookies()).get('session'); + if (!sessionCookie || !sessionCookie.value) { + return null; + } + + const sessionData = await verifyToken(sessionCookie.value); + if ( + !sessionData || + !sessionData.user || + typeof sessionData.user.id !== 'number' + ) { + return null; + } + + if (new Date(sessionData.expires) < new Date()) { + return null; + } + + const user = await db + .select() + .from(users) + .where(and(eq(users.id, sessionData.user.id), isNull(users.deletedAt))) + .limit(1); + + if (user.length === 0) { + return null; + } + + return user[0]; +} + +export async function getTeamByStripeCustomerId(customerId: string) { + const result = await db + .select() + .from(teams) + .where(eq(teams.stripeCustomerId, customerId)) + .limit(1); + + return result.length > 0 ? result[0] : null; +} + +export async function updateTeamSubscription( + teamId: number, + subscriptionData: { + stripeSubscriptionId: string | null; + stripeProductId: string | null; + planName: string | null; + subscriptionStatus: string; + } +) { + await db + .update(teams) + .set({ + ...subscriptionData, + updatedAt: new Date() + }) + .where(eq(teams.id, teamId)); +} + +export async function getUserWithTeam(userId: number) { + const result = await db + .select({ + user: users, + teamId: teamMembers.teamId + }) + .from(users) + .leftJoin(teamMembers, eq(users.id, teamMembers.userId)) + .where(eq(users.id, userId)) + .limit(1); + + return result[0]; +} + +export async function getActivityLogs() { + const user = await getUser(); + if (!user) { + throw new Error('User not authenticated'); + } + + return await db + .select({ + id: activityLogs.id, + action: activityLogs.action, + timestamp: activityLogs.timestamp, + ipAddress: activityLogs.ipAddress, + userName: users.name + }) + .from(activityLogs) + .leftJoin(users, eq(activityLogs.userId, users.id)) + .where(eq(activityLogs.userId, user.id)) + .orderBy(desc(activityLogs.timestamp)) + .limit(10); +} + +export async function getTeamForUser() { + const user = await getUser(); + if (!user) { + return null; + } + + const result = await db.query.teamMembers.findFirst({ + where: eq(teamMembers.userId, user.id), + with: { + team: { + with: { + teamMembers: { + with: { + user: { + columns: { + id: true, + name: true, + email: true + } + } + } + } + } + } + } + }); + + return result?.team || null; +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/db/schema.ts b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/db/schema.ts new file mode 100644 index 000000000..1d047ce66 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/db/schema.ts @@ -0,0 +1,142 @@ +import { + pgTable, + serial, + varchar, + text, + timestamp, + integer, +} from 'drizzle-orm/pg-core'; +import { relations } from 'drizzle-orm'; + +export const users = pgTable('users', { + id: serial('id').primaryKey(), + name: varchar('name', { length: 100 }), + email: varchar('email', { length: 255 }).notNull().unique(), + passwordHash: text('password_hash').notNull(), + role: varchar('role', { length: 20 }).notNull().default('member'), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + deletedAt: timestamp('deleted_at'), +}); + +export const teams = pgTable('teams', { + id: serial('id').primaryKey(), + name: varchar('name', { length: 100 }).notNull(), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + stripeCustomerId: text('stripe_customer_id').unique(), + stripeSubscriptionId: text('stripe_subscription_id').unique(), + stripeProductId: text('stripe_product_id'), + planName: varchar('plan_name', { length: 50 }), + subscriptionStatus: varchar('subscription_status', { length: 20 }), +}); + +export const teamMembers = pgTable('team_members', { + id: serial('id').primaryKey(), + userId: integer('user_id') + .notNull() + .references(() => users.id), + teamId: integer('team_id') + .notNull() + .references(() => teams.id), + role: varchar('role', { length: 50 }).notNull(), + joinedAt: timestamp('joined_at').notNull().defaultNow(), +}); + +export const activityLogs = pgTable('activity_logs', { + id: serial('id').primaryKey(), + teamId: integer('team_id') + .notNull() + .references(() => teams.id), + userId: integer('user_id').references(() => users.id), + action: text('action').notNull(), + timestamp: timestamp('timestamp').notNull().defaultNow(), + ipAddress: varchar('ip_address', { length: 45 }), +}); + +export const invitations = pgTable('invitations', { + id: serial('id').primaryKey(), + teamId: integer('team_id') + .notNull() + .references(() => teams.id), + email: varchar('email', { length: 255 }).notNull(), + role: varchar('role', { length: 50 }).notNull(), + invitedBy: integer('invited_by') + .notNull() + .references(() => users.id), + invitedAt: timestamp('invited_at').notNull().defaultNow(), + status: varchar('status', { length: 20 }).notNull().default('pending'), +}); + +export const teamsRelations = relations(teams, ({ many }) => ({ + teamMembers: many(teamMembers), + activityLogs: many(activityLogs), + invitations: many(invitations), +})); + +export const usersRelations = relations(users, ({ many }) => ({ + teamMembers: many(teamMembers), + invitationsSent: many(invitations), +})); + +export const invitationsRelations = relations(invitations, ({ one }) => ({ + team: one(teams, { + fields: [invitations.teamId], + references: [teams.id], + }), + invitedBy: one(users, { + fields: [invitations.invitedBy], + references: [users.id], + }), +})); + +export const teamMembersRelations = relations(teamMembers, ({ one }) => ({ + user: one(users, { + fields: [teamMembers.userId], + references: [users.id], + }), + team: one(teams, { + fields: [teamMembers.teamId], + references: [teams.id], + }), +})); + +export const activityLogsRelations = relations(activityLogs, ({ one }) => ({ + team: one(teams, { + fields: [activityLogs.teamId], + references: [teams.id], + }), + user: one(users, { + fields: [activityLogs.userId], + references: [users.id], + }), +})); + +export type User = typeof users.$inferSelect; +export type NewUser = typeof users.$inferInsert; +export type Team = typeof teams.$inferSelect; +export type NewTeam = typeof teams.$inferInsert; +export type TeamMember = typeof teamMembers.$inferSelect; +export type NewTeamMember = typeof teamMembers.$inferInsert; +export type ActivityLog = typeof activityLogs.$inferSelect; +export type NewActivityLog = typeof activityLogs.$inferInsert; +export type Invitation = typeof invitations.$inferSelect; +export type NewInvitation = typeof invitations.$inferInsert; +export type TeamDataWithMembers = Team & { + teamMembers: (TeamMember & { + user: Pick; + })[]; +}; + +export enum ActivityType { + SIGN_UP = 'SIGN_UP', + SIGN_IN = 'SIGN_IN', + SIGN_OUT = 'SIGN_OUT', + UPDATE_PASSWORD = 'UPDATE_PASSWORD', + DELETE_ACCOUNT = 'DELETE_ACCOUNT', + UPDATE_ACCOUNT = 'UPDATE_ACCOUNT', + CREATE_TEAM = 'CREATE_TEAM', + REMOVE_TEAM_MEMBER = 'REMOVE_TEAM_MEMBER', + INVITE_TEAM_MEMBER = 'INVITE_TEAM_MEMBER', + ACCEPT_INVITATION = 'ACCEPT_INVITATION', +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/db/seed.ts b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/db/seed.ts new file mode 100644 index 000000000..66b60513e --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/db/seed.ts @@ -0,0 +1,118 @@ +import { stripe } from '../payments/stripe'; +import { db } from './drizzle'; +import { users, teams, teamMembers } from './schema'; +import { hashPassword } from '@/lib/auth/session'; +import { eq } from 'drizzle-orm'; + +async function createStripeProducts() { + console.log('Creating Stripe products and prices...'); + + const baseProduct = await stripe.products.create({ + name: 'Base', + description: 'Base subscription plan', + }); + + await stripe.prices.create({ + product: baseProduct.id, + unit_amount: 800, // $8 in cents + currency: 'usd', + recurring: { + interval: 'month', + trial_period_days: 7, + }, + }); + + const plusProduct = await stripe.products.create({ + name: 'Plus', + description: 'Plus subscription plan', + }); + + await stripe.prices.create({ + product: plusProduct.id, + unit_amount: 1200, // $12 in cents + currency: 'usd', + recurring: { + interval: 'month', + trial_period_days: 7, + }, + }); + + console.log('Stripe products and prices created successfully.'); +} + +async function seed() { + const email = 'test@test.com'; + const password = 'admin123'; + const passwordHash = await hashPassword(password); + + // Check if user already exists + const existingUsers = await db + .select() + .from(users) + .where(eq(users.email, email)) + .limit(1); + + let user; + if (existingUsers.length > 0) { + console.log('User already exists, skipping user creation.'); + user = existingUsers[0]; + } else { + const [newUser] = await db + .insert(users) + .values([ + { + email: email, + passwordHash: passwordHash, + role: "owner", + }, + ]) + .returning(); + user = newUser; + console.log('Initial user created.'); + } + + // Check if team already exists for this user + const existingTeamMembers = await db + .select() + .from(teamMembers) + .where(eq(teamMembers.userId, user.id)) + .limit(1); + + let team; + if (existingTeamMembers.length > 0) { + console.log('Team membership already exists, skipping team creation.'); + const existingTeams = await db + .select() + .from(teams) + .where(eq(teams.id, existingTeamMembers[0].teamId)) + .limit(1); + team = existingTeams[0]; + } else { + const [newTeam] = await db + .insert(teams) + .values({ + name: 'Test Team', + }) + .returning(); + team = newTeam; + + await db.insert(teamMembers).values({ + teamId: team.id, + userId: user.id, + role: 'owner', + }); + console.log('Team and team membership created.'); + } + + await createStripeProducts(); +} + +seed() + .catch((error) => { + console.error('Seed process failed:', error); + process.exit(1); + }) + .finally(() => { + console.log('Seed process finished. Exiting...'); + process.exit(0); + }); diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/db/setup.ts b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/db/setup.ts new file mode 100644 index 000000000..c23d7d4fa --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/db/setup.ts @@ -0,0 +1,216 @@ +import { exec } from 'node:child_process'; +import { promises as fs } from 'node:fs'; +import { promisify } from 'node:util'; +import readline from 'node:readline'; +import crypto from 'node:crypto'; +import path from 'node:path'; +import os from 'node:os'; + +const execAsync = promisify(exec); + +function question(query: string): Promise { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + return new Promise((resolve) => + rl.question(query, (ans) => { + rl.close(); + resolve(ans); + }) + ); +} + +async function checkStripeCLI() { + console.log( + 'Step 1: Checking if Stripe CLI is installed and authenticated...' + ); + try { + await execAsync('stripe --version'); + console.log('Stripe CLI is installed.'); + + // Check if Stripe CLI is authenticated + try { + await execAsync('stripe config --list'); + console.log('Stripe CLI is authenticated.'); + } catch (error) { + console.log( + 'Stripe CLI is not authenticated or the authentication has expired.' + ); + console.log('Please run: stripe login'); + const answer = await question( + 'Have you completed the authentication? (y/n): ' + ); + if (answer.toLowerCase() !== 'y') { + console.log( + 'Please authenticate with Stripe CLI and run this script again.' + ); + process.exit(1); + } + + // Verify authentication after user confirms login + try { + await execAsync('stripe config --list'); + console.log('Stripe CLI authentication confirmed.'); + } catch (error) { + console.error( + 'Failed to verify Stripe CLI authentication. Please try again.' + ); + process.exit(1); + } + } + } catch (error) { + console.error( + 'Stripe CLI is not installed. Please install it and try again.' + ); + console.log('To install Stripe CLI, follow these steps:'); + console.log('1. Visit: https://docs.stripe.com/stripe-cli'); + console.log( + '2. Download and install the Stripe CLI for your operating system' + ); + console.log('3. After installation, run: stripe login'); + console.log( + 'After installation and authentication, please run this setup script again.' + ); + process.exit(1); + } +} + +async function getPostgresURL(): Promise { + console.log('Step 2: Setting up Postgres'); + const dbChoice = await question( + 'Do you want to use a local Postgres instance with Docker (L) or a remote Postgres instance (R)? (L/R): ' + ); + + if (dbChoice.toLowerCase() === 'l') { + console.log('Setting up local Postgres instance with Docker...'); + await setupLocalPostgres(); + return 'postgres://postgres:postgres@localhost:54322/postgres'; + } else { + console.log( + 'You can find Postgres databases at: https://vercel.com/marketplace?category=databases' + ); + return await question('Enter your POSTGRES_URL: '); + } +} + +async function setupLocalPostgres() { + console.log('Checking if Docker is installed...'); + try { + await execAsync('docker --version'); + console.log('Docker is installed.'); + } catch (error) { + console.error( + 'Docker is not installed. Please install Docker and try again.' + ); + console.log( + 'To install Docker, visit: https://docs.docker.com/get-docker/' + ); + process.exit(1); + } + + console.log('Creating docker-compose.yml file...'); + const dockerComposeContent = ` +services: + postgres: + image: postgres:16.4-alpine + container_name: next_saas_starter_postgres + environment: + POSTGRES_DB: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "54322:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + +volumes: + postgres_data: +`; + + await fs.writeFile( + path.join(process.cwd(), 'docker-compose.yml'), + dockerComposeContent + ); + console.log('docker-compose.yml file created.'); + + console.log('Starting Docker container with `docker compose up -d`...'); + try { + await execAsync('docker compose up -d'); + console.log('Docker container started successfully.'); + } catch (error) { + console.error( + 'Failed to start Docker container. Please check your Docker installation and try again.' + ); + process.exit(1); + } +} + +async function getStripeSecretKey(): Promise { + console.log('Step 3: Getting Stripe Secret Key'); + console.log( + 'You can find your Stripe Secret Key at: https://dashboard.stripe.com/test/apikeys' + ); + return await question('Enter your Stripe Secret Key: '); +} + +async function createStripeWebhook(): Promise { + console.log('Step 4: Creating Stripe webhook...'); + try { + const { stdout } = await execAsync('stripe listen --print-secret'); + const match = stdout.match(/whsec_[a-zA-Z0-9]+/); + if (!match) { + throw new Error('Failed to extract Stripe webhook secret'); + } + console.log('Stripe webhook created.'); + return match[0]; + } catch (error) { + console.error( + 'Failed to create Stripe webhook. Check your Stripe CLI installation and permissions.' + ); + if (os.platform() === 'win32') { + console.log( + 'Note: On Windows, you may need to run this script as an administrator.' + ); + } + throw error; + } +} + +function generateAuthSecret(): string { + console.log('Step 5: Generating AUTH_SECRET...'); + return crypto.randomBytes(32).toString('hex'); +} + +async function writeEnvFile(envVars: Record) { + console.log('Step 6: Writing environment variables to .env'); + const envContent = Object.entries(envVars) + .map(([key, value]) => `${key}=${value}`) + .join('\n'); + + await fs.writeFile(path.join(process.cwd(), '.env'), envContent); + console.log('.env file created with the necessary variables.'); +} + +async function main() { + await checkStripeCLI(); + + const POSTGRES_URL = await getPostgresURL(); + const STRIPE_SECRET_KEY = await getStripeSecretKey(); + const STRIPE_WEBHOOK_SECRET = await createStripeWebhook(); + const BASE_URL = 'http://localhost:3000'; + const AUTH_SECRET = generateAuthSecret(); + + await writeEnvFile({ + POSTGRES_URL, + STRIPE_SECRET_KEY, + STRIPE_WEBHOOK_SECRET, + BASE_URL, + AUTH_SECRET, + }); + + console.log('🎉 Setup completed successfully!'); +} + +main().catch(console.error); diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/payments/STRIPE_STUB_README.md b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/payments/STRIPE_STUB_README.md new file mode 100644 index 000000000..80111f5ca --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/payments/STRIPE_STUB_README.md @@ -0,0 +1,212 @@ +# Stripe Stub Mode + +This directory contains a stub implementation of the Stripe API that allows you to develop and test the SaaS application without real Stripe API keys. + +## Why Use Stub Mode? + +- **No Stripe Account Required**: Develop without signing up for Stripe +- **Faster Development**: No network latency from API calls +- **Offline Development**: Work without internet connection +- **Predictable Testing**: Consistent mock data for testing +- **No API Limits**: No rate limiting or quotas +- **Free**: No charges or test mode transactions + +## How to Enable Stub Mode + +### Option 1: Environment Variable (Recommended) + +Add to your [.env](.env) file: + +```bash +STRIPE_MODE=stub +``` + +### Option 2: Invalid/Missing API Keys + +The stub mode automatically activates if: +- `STRIPE_SECRET_KEY` is not set +- `STRIPE_SECRET_KEY` is set to `sk_test_***` (example value) + +## What Gets Mocked + +### Products & Prices + +The stub provides three pre-configured products: + +1. **Basic Plan** + - Product ID: `prod_stub_basic` + - Price ID: `price_stub_basic_monthly` + - Price: $10.00/month + - Trial: 14 days + +2. **Pro Plan** + - Product ID: `prod_stub_pro` + - Price ID: `price_stub_pro_monthly` + - Price: $25.00/month + - Trial: 14 days + +3. **Enterprise Plan** + - Product ID: `prod_stub_enterprise` + - Price ID: `price_stub_enterprise_monthly` + - Price: $100.00/month + - Trial: 14 days + +### Checkout Sessions + +When you create a checkout session in stub mode: +- A mock session ID is generated (`cs_stub_*`) +- A mock customer ID is created (`cus_stub_*`) +- A mock subscription ID is created (`sub_stub_*`) +- The user is redirected to the success URL +- All subscription data is stored in your local database + +### Customer Portal + +The billing portal session creates: +- A mock portal session ID (`bps_stub_*`) +- A redirect to your dashboard with a query param +- All configuration options are accepted + +### Webhooks + +Webhook events can be simulated by: +1. Calling the webhook endpoint directly +2. Sending mock event data in the request body +3. The stub will parse and process the event + +Example webhook event: +```json +{ + "type": "customer.subscription.updated", + "data": { + "object": { + "id": "sub_stub_123", + "customer": "cus_stub_123", + "status": "active", + "items": { + "data": [{ + "plan": { + "product": { + "id": "prod_stub_basic", + "name": "Basic Plan" + } + } + }] + } + } + } +} +``` + +## Testing the Checkout Flow + +1. **Navigate to Pricing Page**: `/pricing` +2. **Click on a Plan**: Choose any plan +3. **Get Redirected**: You'll see a "checkout" URL with session ID +4. **Auto-Complete**: The checkout automatically succeeds +5. **Return to Dashboard**: You're logged in with active subscription + +The entire flow works without leaving your application! + +## Console Logging + +When stub mode is active, you'll see: +``` +🎭 Running in Stripe STUB mode - no real API calls will be made +``` + +All stub operations log to console with `[Stripe Stub]` prefix: +``` +[Stripe Stub] Creating checkout session: {...} +[Stripe Stub] Retrieving subscription: sub_stub_123 +[Stripe Stub] Listing products: {...} +``` + +## Subscription Statuses + +The stub supports all subscription statuses: +- `trialing` (default for new subscriptions) +- `active` +- `canceled` +- `unpaid` +- `past_due` + +## Limitations + +The stub implementation: +- ✅ Supports all core SaaS functionality +- ✅ Works with the pricing page +- ✅ Processes checkout sessions +- ✅ Handles webhooks +- ✅ Manages customer portal +- ❌ Does NOT process real payments +- ❌ Does NOT send Stripe emails +- ❌ Does NOT create actual Stripe resources +- ❌ Does NOT validate real payment methods + +## Switching to Real Stripe + +When you're ready to use real Stripe: + +1. Sign up at [stripe.com](https://stripe.com) +2. Get your API keys from the Stripe Dashboard +3. Update your `.env` file: + ```bash + STRIPE_SECRET_KEY=sk_test_your_real_key + STRIPE_WEBHOOK_SECRET=whsec_your_real_secret + # Remove or comment out: + # STRIPE_MODE=stub + ``` +4. Restart your development server +5. Set up real products and prices in Stripe Dashboard +6. Configure webhook endpoints in Stripe + +## Files + +- [stripe-stub.ts](stripe-stub.ts) - Stub implementation +- [stripe.ts](stripe.ts) - Main Stripe client (auto-detects stub mode) +- `STRIPE_STUB_README.md` - This file + +## Development Tips + +1. **Use stub mode for UI development** - Build out forms, buttons, and flows +2. **Use real Stripe for payment testing** - Verify actual payment processing +3. **Keep stub data simple** - Extend the stub as needed for your use case +4. **Log everything** - The stub logs all operations for debugging + +## Extending the Stub + +To add more mock products or customize behavior, edit [stripe-stub.ts](stripe-stub.ts): + +```typescript +// Add a new product +{ + id: 'prod_stub_custom', + name: 'Custom Plan', + description: 'A custom plan', + active: true, +} +``` + +## Troubleshooting + +**Q: My checkout isn't working** +A: Check the console for `[Stripe Stub]` logs to see what's happening + +**Q: I'm not seeing stub mode** +A: Verify `STRIPE_MODE=stub` in your `.env` and restart the dev server + +**Q: Can I use this in production?** +A: No! Stub mode is for development only. Always use real Stripe in production. + +**Q: How do I test webhooks?** +A: Send POST requests to `/api/stripe/webhook` with mock event data + +## Support + +For issues with the stub implementation, check: +1. Console logs for `[Stripe Stub]` messages +2. Your `.env` configuration +3. The stub source code in [stripe-stub.ts](stripe-stub.ts) + +For real Stripe issues, see [Stripe Documentation](https://stripe.com/docs). diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/payments/TESTING_GUIDE.md b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/payments/TESTING_GUIDE.md new file mode 100644 index 000000000..c991dde04 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/payments/TESTING_GUIDE.md @@ -0,0 +1,223 @@ +# Testing Stripe Stub Mode + +This guide shows you how to test the application using Stripe stub mode. + +## Quick Start + +1. **Enable stub mode** (already done in your `.env`): + ```bash + STRIPE_MODE=stub + ``` + +2. **Start your development server**: + ```bash + pnpm dev + ``` + +3. **Look for the stub mode indicator** in your console: + ``` + 🎭 Running in Stripe STUB mode - no real API calls will be made + ``` + +## Testing Scenarios + +### Scenario 1: View Pricing + +1. Navigate to `http://localhost:3000/pricing` +2. You should see pricing plans displayed +3. The plans come from the stub (not real Stripe) + +**Expected Stub Data:** +- Basic Plan: $10/month +- Pro Plan: $25/month +- Enterprise Plan: $100/month + +### Scenario 2: Checkout Flow + +1. Go to `/pricing` +2. Click "Subscribe" on any plan +3. You'll be redirected to checkout +4. The stub automatically completes checkout +5. You land on `/dashboard` with an active subscription + +**What happens behind the scenes:** +``` +[Stripe Stub] Creating checkout session +[Stripe Stub] Retrieving checkout session +[Stripe Stub] Retrieving subscription +``` + +**Database changes:** +- Team gets `stripeCustomerId` (e.g., `cus_stub_123456`) +- Team gets `stripeSubscriptionId` (e.g., `sub_stub_123456`) +- Team gets `stripeProductId` (e.g., `prod_stub_basic`) +- Team `subscriptionStatus` set to `trialing` + +### Scenario 3: Customer Portal + +1. From your dashboard, click "Manage Subscription" +2. You'll be redirected to a stub portal URL +3. The URL contains `?stub=billing-portal` query param + +**Expected behavior:** +- No actual Stripe portal is opened +- You stay on your local application +- Can simulate subscription changes via webhooks + +### Scenario 4: Webhook Simulation + +Use curl or Postman to simulate a webhook: + +```bash +curl -X POST http://localhost:3000/api/stripe/webhook \ + -H "Content-Type: application/json" \ + -H "stripe-signature: stub_signature" \ + -d '{ + "type": "customer.subscription.updated", + "data": { + "object": { + "id": "sub_stub_123", + "customer": "cus_stub_123", + "status": "active", + "items": { + "data": [{ + "plan": { + "product": { + "id": "prod_stub_basic", + "name": "Basic Plan" + } + } + }] + } + } + } + }' +``` + +**Expected response:** +```json +{ "received": true } +``` + +### Scenario 5: Subscription Cancellation + +Simulate a cancellation webhook: + +```bash +curl -X POST http://localhost:3000/api/stripe/webhook \ + -H "Content-Type: application/json" \ + -H "stripe-signature: stub_signature" \ + -d '{ + "type": "customer.subscription.deleted", + "data": { + "object": { + "id": "sub_stub_123", + "customer": "cus_stub_123", + "status": "canceled" + } + } + }' +``` + +**Expected database changes:** +- `stripeSubscriptionId` → `null` +- `stripeProductId` → `null` +- `planName` → `null` +- `subscriptionStatus` → `canceled` + +## Debugging Tips + +### Enable Verbose Logging + +All stub operations log to console. Look for: +``` +[Stripe Stub] Creating checkout session: { priceId: 'price_stub_basic_monthly', ... } +[Stripe Stub] Retrieving subscription: sub_stub_123456 +``` + +### Check Database State + +After any operation, check your database: + +```sql +SELECT + id, + name, + stripeCustomerId, + stripeSubscriptionId, + stripeProductId, + planName, + subscriptionStatus +FROM teams; +``` + +### Common Issues + +**Issue: Not seeing stub mode message** +- ✅ Solution: Make sure `.env` has `STRIPE_MODE=stub` +- ✅ Solution: Restart your dev server + +**Issue: Checkout redirects to real Stripe** +- ✅ Solution: Check console for stub mode indicator +- ✅ Solution: Verify `STRIPE_MODE=stub` is set correctly + +**Issue: Webhook fails** +- ✅ Solution: Include `stripe-signature` header (any value works) +- ✅ Solution: Check request body matches expected format + +## Comparing Stub vs Real Stripe + +| Feature | Stub Mode | Real Stripe | +|---------|-----------|-------------| +| Speed | Instant ⚡ | Network latency | +| Cost | Free 💰 | Test mode free, production paid | +| Internet Required | No 📡 | Yes | +| Payment Processing | Simulated ✨ | Real | +| Webhook Events | Manual 🔧 | Automatic | +| Customer Portal | Mocked 🎭 | Real Stripe UI | +| Product Management | Hardcoded 📝 | Dashboard managed | + +## When to Use Each Mode + +### Use Stub Mode For: +- ✅ Initial development +- ✅ UI/UX work +- ✅ Offline development +- ✅ Fast iteration +- ✅ Testing subscription logic +- ✅ CI/CD pipelines + +### Use Real Stripe For: +- ✅ Payment flow testing +- ✅ Webhook integration testing +- ✅ Customer portal testing +- ✅ Pre-production testing +- ✅ Production deployment + +## Switching Between Modes + +### Enable Stub Mode +```bash +# In .env +STRIPE_MODE=stub +``` + +### Enable Real Stripe +```bash +# In .env +# STRIPE_MODE=stub # Comment out or remove this line +``` + +Then restart your dev server. + +## Next Steps + +1. **Build your features** using stub mode +2. **Test thoroughly** with real Stripe in test mode +3. **Deploy to production** with real Stripe in live mode + +## API Reference + +See [stripe-stub.ts](stripe-stub.ts) for complete stub implementation details. + +For real Stripe API, see [Stripe API Docs](https://stripe.com/docs/api). diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/payments/actions.ts b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/payments/actions.ts new file mode 100644 index 000000000..26492c19e --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/payments/actions.ts @@ -0,0 +1,15 @@ +'use server'; + +import { redirect } from 'next/navigation'; +import { createCheckoutSession, createCustomerPortalSession } from './stripe'; +import { withTeam } from '@/lib/auth/middleware'; + +export const checkoutAction = withTeam(async (formData, team) => { + const priceId = formData.get('priceId') as string; + await createCheckoutSession({ team: team, priceId }); +}); + +export const customerPortalAction = withTeam(async (_, team) => { + const portalSession = await createCustomerPortalSession(team); + redirect(portalSession.url); +}); diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/payments/stripe-stub.ts b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/payments/stripe-stub.ts new file mode 100644 index 000000000..270a91322 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/payments/stripe-stub.ts @@ -0,0 +1,354 @@ +/** + * Stripe Stub Implementation + * + * This file provides mock implementations of Stripe API calls for development + * without requiring real Stripe API keys. + * + * To use: Set STRIPE_MODE=stub in your .env file + */ + +export interface StubCheckoutSession { + id: string; + url: string; + customer: { id: string }; + subscription: string; + client_reference_id: string; + payment_method_types: string[]; + line_items: Array<{ price: string; quantity: number }>; + mode: string; + success_url: string; + cancel_url: string; + allow_promotion_codes: boolean; + subscription_data?: { trial_period_days: number }; +} + +export interface StubBillingPortalSession { + id: string; + url: string; + customer: string; + return_url: string; + configuration: string; +} + +export interface StubSubscription { + id: string; + customer: string; + status: 'active' | 'trialing' | 'canceled' | 'unpaid' | 'past_due'; + items: { + data: Array<{ + id: string; + price: StubPrice; + plan?: StubPrice; + }>; + }; +} + +export interface StubPrice { + id: string; + product: string | StubProduct; + unit_amount: number; + currency: string; + recurring?: { + interval: 'day' | 'week' | 'month' | 'year'; + trial_period_days?: number; + }; +} + +export interface StubProduct { + id: string; + name: string; + description: string | null; + active: boolean; + default_price?: string | StubPrice; +} + +export interface StubBillingPortalConfiguration { + id: string; + business_profile: { headline: string }; + features: Record; +} + +export interface StubEvent { + id: string; + type: string; + data: { + object: StubSubscription | Record; + }; +} + +/** + * Stub Stripe Client + * Mimics the Stripe SDK structure but returns mock data + */ +export class StripeStub { + checkout = { + sessions: { + create: async (params: Record): Promise => { + const sessionId = `cs_stub_${Date.now()}`; + const customerId = (params.customer as string) || `cus_stub_${Date.now()}`; + const subscriptionId = `sub_stub_${Date.now()}`; + + console.log('[Stripe Stub] Creating checkout session:', params); + + return { + id: sessionId, + url: `${params.success_url}`.replace('{CHECKOUT_SESSION_ID}', sessionId), + customer: { id: customerId }, + subscription: subscriptionId, + client_reference_id: params.client_reference_id as string, + payment_method_types: params.payment_method_types as string[], + line_items: params.line_items as Array<{ price: string; quantity: number }>, + mode: params.mode as string, + success_url: params.success_url as string, + cancel_url: params.cancel_url as string, + allow_promotion_codes: params.allow_promotion_codes as boolean, + subscription_data: params.subscription_data as { trial_period_days: number } | undefined, + }; + }, + retrieve: async ( + sessionId: string, + options?: { expand?: string[] } + ): Promise => { + console.log('[Stripe Stub] Retrieving checkout session:', sessionId); + + const customerId = `cus_stub_${Date.now()}`; + const subscriptionId = `sub_stub_${Date.now()}`; + + return { + id: sessionId, + url: `http://localhost:3000/dashboard`, + customer: { id: customerId }, + subscription: subscriptionId, + client_reference_id: '1', // Default user ID + payment_method_types: ['card'], + line_items: [{ price: 'price_stub_basic', quantity: 1 }], + mode: 'subscription', + success_url: 'http://localhost:3000/api/stripe/checkout', + cancel_url: 'http://localhost:3000/pricing', + allow_promotion_codes: true, + subscription_data: { trial_period_days: 14 }, + }; + }, + }, + }; + + subscriptions = { + retrieve: async ( + subscriptionId: string, + options?: { expand?: string[] } + ): Promise => { + console.log('[Stripe Stub] Retrieving subscription:', subscriptionId); + + return { + id: subscriptionId, + customer: `cus_stub_${Date.now()}`, + status: 'trialing', + items: { + data: [ + { + id: 'si_stub_1', + price: { + id: 'price_stub_basic', + product: { + id: 'prod_stub_basic', + name: 'Basic Plan', + description: 'Basic tier subscription', + active: true, + }, + unit_amount: 1000, + currency: 'usd', + recurring: { + interval: 'month', + trial_period_days: 14, + }, + }, + }, + ], + }, + }; + }, + }; + + billingPortal = { + configurations: { + list: async (): Promise<{ data: StubBillingPortalConfiguration[] }> => { + console.log('[Stripe Stub] Listing billing portal configurations'); + return { + data: [ + { + id: 'bpc_stub_1', + business_profile: { headline: 'Manage your subscription' }, + features: {}, + }, + ], + }; + }, + create: async (params: Record): Promise => { + console.log('[Stripe Stub] Creating billing portal configuration:', params); + return { + id: `bpc_stub_${Date.now()}`, + business_profile: params.business_profile as { headline: string }, + features: params.features as Record, + }; + }, + }, + sessions: { + create: async (params: { + customer: string; + return_url: string; + configuration?: string; + }): Promise => { + console.log('[Stripe Stub] Creating billing portal session:', params); + return { + id: `bps_stub_${Date.now()}`, + url: `http://localhost:3000/dashboard?stub=billing-portal`, + customer: params.customer, + return_url: params.return_url, + configuration: params.configuration || 'bpc_stub_default', + }; + }, + }, + }; + + products = { + list: async (params?: { + active?: boolean; + expand?: string[]; + }): Promise<{ data: StubProduct[] }> => { + console.log('[Stripe Stub] Listing products:', params); + return { + data: [ + { + id: 'prod_stub_basic', + name: 'Basic Plan', + description: 'Perfect for getting started', + active: true, + default_price: 'price_stub_basic_monthly', + }, + { + id: 'prod_stub_pro', + name: 'Pro Plan', + description: 'For growing teams', + active: true, + default_price: 'price_stub_pro_monthly', + }, + { + id: 'prod_stub_enterprise', + name: 'Enterprise Plan', + description: 'For large organizations', + active: true, + default_price: 'price_stub_enterprise_monthly', + }, + ], + }; + }, + retrieve: async (productId: string): Promise => { + console.log('[Stripe Stub] Retrieving product:', productId); + return { + id: productId, + name: 'Basic Plan', + description: 'Perfect for getting started', + active: true, + }; + }, + }; + + prices = { + list: async (params?: { + active?: boolean; + type?: string; + product?: string; + expand?: string[]; + }): Promise<{ data: StubPrice[] }> => { + console.log('[Stripe Stub] Listing prices:', params); + return { + data: [ + { + id: 'price_stub_basic_monthly', + product: { + id: 'prod_stub_basic', + name: 'Basic Plan', + description: 'Perfect for getting started', + active: true, + }, + unit_amount: 1000, // $10.00 + currency: 'usd', + recurring: { + interval: 'month', + trial_period_days: 14, + }, + }, + { + id: 'price_stub_pro_monthly', + product: { + id: 'prod_stub_pro', + name: 'Pro Plan', + description: 'For growing teams', + active: true, + }, + unit_amount: 2500, // $25.00 + currency: 'usd', + recurring: { + interval: 'month', + trial_period_days: 14, + }, + }, + { + id: 'price_stub_enterprise_monthly', + product: { + id: 'prod_stub_enterprise', + name: 'Enterprise Plan', + description: 'For large organizations', + active: true, + }, + unit_amount: 10000, // $100.00 + currency: 'usd', + recurring: { + interval: 'month', + trial_period_days: 14, + }, + }, + ], + }; + }, + }; + + webhooks = { + constructEvent: ( + payload: string, + signature: string, + secret: string + ): StubEvent => { + console.log('[Stripe Stub] Constructing webhook event'); + + // Parse the payload + const parsedPayload = JSON.parse(payload); + + return { + id: `evt_stub_${Date.now()}`, + type: parsedPayload.type || 'customer.subscription.updated', + data: { + object: parsedPayload.data?.object || { + id: `sub_stub_${Date.now()}`, + customer: `cus_stub_${Date.now()}`, + status: 'active', + items: { + data: [ + { + plan: { + product: { + id: 'prod_stub_basic', + name: 'Basic Plan', + }, + }, + }, + ], + }, + }, + }, + }; + }, + }; +} + +export const stripeStub = new StripeStub(); diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/payments/stripe.ts b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/payments/stripe.ts new file mode 100644 index 000000000..291e6ba06 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/payments/stripe.ts @@ -0,0 +1,195 @@ +import Stripe from 'stripe'; +import { redirect } from 'next/navigation'; +import { Team } from '@/lib/db/schema'; +import { + getTeamByStripeCustomerId, + getUser, + updateTeamSubscription +} from '@/lib/db/queries'; +import { stripeStub } from './stripe-stub'; + +// Use stub if STRIPE_MODE=stub or if STRIPE_SECRET_KEY is missing/invalid +const useStub = + process.env.STRIPE_MODE === 'stub' || + !process.env.STRIPE_SECRET_KEY || + process.env.STRIPE_SECRET_KEY === 'sk_test_***'; + +export const stripe = useStub + ? (stripeStub as unknown as Stripe) + : new Stripe(process.env.STRIPE_SECRET_KEY!, { + apiVersion: '2025-04-30.basil' + }); + +if (useStub) { + console.log('🎭 Running in Stripe STUB mode - no real API calls will be made'); +} + +export async function createCheckoutSession({ + team, + priceId +}: { + team: Team | null; + priceId: string; +}) { + const user = await getUser(); + + if (!team || !user) { + redirect(`/sign-up?redirect=checkout&priceId=${priceId}`); + } + + const session = await stripe.checkout.sessions.create({ + payment_method_types: ['card'], + line_items: [ + { + price: priceId, + quantity: 1 + } + ], + mode: 'subscription', + success_url: `${process.env.BASE_URL}/api/stripe/checkout?session_id={CHECKOUT_SESSION_ID}`, + cancel_url: `${process.env.BASE_URL}/pricing`, + customer: team.stripeCustomerId || undefined, + client_reference_id: user.id.toString(), + allow_promotion_codes: true, + subscription_data: { + trial_period_days: 14 + } + }); + + redirect(session.url!); +} + +export async function createCustomerPortalSession(team: Team) { + if (!team.stripeCustomerId || !team.stripeProductId) { + redirect('/pricing'); + } + + let configuration: Stripe.BillingPortal.Configuration; + const configurations = await stripe.billingPortal.configurations.list(); + + if (configurations.data.length > 0) { + configuration = configurations.data[0]; + } else { + const product = await stripe.products.retrieve(team.stripeProductId); + if (!product.active) { + throw new Error("Team's product is not active in Stripe"); + } + + const prices = await stripe.prices.list({ + product: product.id, + active: true + }); + if (prices.data.length === 0) { + throw new Error("No active prices found for the team's product"); + } + + configuration = await stripe.billingPortal.configurations.create({ + business_profile: { + headline: 'Manage your subscription' + }, + features: { + subscription_update: { + enabled: true, + default_allowed_updates: ['price', 'quantity', 'promotion_code'], + proration_behavior: 'create_prorations', + products: [ + { + product: product.id, + prices: prices.data.map((price) => price.id) + } + ] + }, + subscription_cancel: { + enabled: true, + mode: 'at_period_end', + cancellation_reason: { + enabled: true, + options: [ + 'too_expensive', + 'missing_features', + 'switched_service', + 'unused', + 'other' + ] + } + }, + payment_method_update: { + enabled: true + } + } + }); + } + + return stripe.billingPortal.sessions.create({ + customer: team.stripeCustomerId, + return_url: `${process.env.BASE_URL}/dashboard`, + configuration: configuration.id + }); +} + +export async function handleSubscriptionChange( + subscription: Stripe.Subscription +) { + const customerId = subscription.customer as string; + const subscriptionId = subscription.id; + const status = subscription.status; + + const team = await getTeamByStripeCustomerId(customerId); + + if (!team) { + console.error('Team not found for Stripe customer:', customerId); + return; + } + + if (status === 'active' || status === 'trialing') { + const plan = subscription.items.data[0]?.plan; + await updateTeamSubscription(team.id, { + stripeSubscriptionId: subscriptionId, + stripeProductId: plan?.product as string, + planName: (plan?.product as Stripe.Product).name, + subscriptionStatus: status + }); + } else if (status === 'canceled' || status === 'unpaid') { + await updateTeamSubscription(team.id, { + stripeSubscriptionId: null, + stripeProductId: null, + planName: null, + subscriptionStatus: status + }); + } +} + +export async function getStripePrices() { + const prices = await stripe.prices.list({ + expand: ['data.product'], + active: true, + type: 'recurring' + }); + + return prices.data.map((price) => ({ + id: price.id, + productId: + typeof price.product === 'string' ? price.product : price.product.id, + unitAmount: price.unit_amount, + currency: price.currency, + interval: price.recurring?.interval, + trialPeriodDays: price.recurring?.trial_period_days + })); +} + +export async function getStripeProducts() { + const products = await stripe.products.list({ + active: true, + expand: ['data.default_price'] + }); + + return products.data.map((product) => ({ + id: product.id, + name: product.name, + description: product.description, + defaultPriceId: + typeof product.default_price === 'string' + ? product.default_price + : product.default_price?.id + })); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/utils.ts b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/utils.ts new file mode 100644 index 000000000..bd0c391dd --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/middleware.ts b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/middleware.ts new file mode 100644 index 000000000..1c8a638e4 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/middleware.ts @@ -0,0 +1,49 @@ +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; +import { signToken, verifyToken } from '@/lib/auth/session'; + +const protectedRoutes = '/dashboard'; + +export async function middleware(request: NextRequest) { + const { pathname } = request.nextUrl; + const sessionCookie = request.cookies.get('session'); + const isProtectedRoute = pathname.startsWith(protectedRoutes); + + if (isProtectedRoute && !sessionCookie) { + return NextResponse.redirect(new URL('/sign-in', request.url)); + } + + let res = NextResponse.next(); + + if (sessionCookie && request.method === 'GET') { + try { + const parsed = await verifyToken(sessionCookie.value); + const expiresInOneDay = new Date(Date.now() + 24 * 60 * 60 * 1000); + + res.cookies.set({ + name: 'session', + value: await signToken({ + ...parsed, + expires: expiresInOneDay.toISOString() + }), + httpOnly: true, + secure: true, + sameSite: 'lax', + expires: expiresInOneDay + }); + } catch (error) { + console.error('Error updating session:', error); + res.cookies.delete('session'); + if (isProtectedRoute) { + return NextResponse.redirect(new URL('/sign-in', request.url)); + } + } + } + + return res; +} + +export const config = { + matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], + runtime: 'nodejs' +}; diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/next.config.ts b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/next.config.ts new file mode 100644 index 000000000..fd4fd4546 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/next.config.ts @@ -0,0 +1,9 @@ +import type { NextConfig } from 'next'; + +const nextConfig: NextConfig = { + // Configuration for stable Next.js 15 + // To enable experimental features like PPR, upgrade to canary: + // pnpm add next@canary +}; + +export default nextConfig; diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/package.json b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/package.json new file mode 100644 index 000000000..020d334a4 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/package.json @@ -0,0 +1,42 @@ +{ + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "db:setup": "npx tsx lib/db/setup.ts", + "db:seed": "npx tsx lib/db/seed.ts", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:studio": "drizzle-kit studio" + }, + "dependencies": { + "@tailwindcss/postcss": "4.1.7", + "@types/node": "^22.15.18", + "@types/react": "19.1.4", + "@types/react-dom": "19.1.5", + "autoprefixer": "^10.4.21", + "bcryptjs": "^3.0.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "dotenv": "^16.5.0", + "drizzle-kit": "^0.31.1", + "drizzle-orm": "^0.43.1", + "jose": "^6.0.11", + "lucide-react": "^0.511.0", + "next": "15.5.7", + "postcss": "^8.5.3", + "postgres": "^3.4.5", + "radix-ui": "^1.4.2", + "react": "19.1.2", + "react-dom": "19.1.2", + "server-only": "^0.0.1", + "stripe": "^18.1.0", + "swr": "^2.3.3", + "tailwind-merge": "^3.3.0", + "tailwindcss": "4.1.7", + "tw-animate-css": "^1.3.0", + "typescript": "^5.8.3", + "zod": "^3.24.4" + } +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/pnpm-lock.yaml b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/pnpm-lock.yaml new file mode 100644 index 000000000..c4753ff95 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/pnpm-lock.yaml @@ -0,0 +1,3794 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@tailwindcss/postcss': + specifier: 4.1.7 + version: 4.1.7 + '@types/node': + specifier: ^22.15.18 + version: 22.15.18 + '@types/react': + specifier: 19.1.4 + version: 19.1.4 + '@types/react-dom': + specifier: 19.1.5 + version: 19.1.5(@types/react@19.1.4) + autoprefixer: + specifier: ^10.4.21 + version: 10.4.21(postcss@8.5.3) + bcryptjs: + specifier: ^3.0.2 + version: 3.0.2 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + dotenv: + specifier: ^16.5.0 + version: 16.5.0 + drizzle-kit: + specifier: ^0.31.1 + version: 0.31.1 + drizzle-orm: + specifier: ^0.43.1 + version: 0.43.1(gel@2.1.0)(postgres@3.4.5) + jose: + specifier: ^6.0.11 + version: 6.0.11 + lucide-react: + specifier: ^0.511.0 + version: 0.511.0(react@19.1.2) + next: + specifier: 15.5.7 + version: 15.5.7(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + postcss: + specifier: ^8.5.3 + version: 8.5.3 + postgres: + specifier: ^3.4.5 + version: 3.4.5 + radix-ui: + specifier: ^1.4.2 + version: 1.4.2(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: + specifier: 19.1.2 + version: 19.1.2 + react-dom: + specifier: 19.1.2 + version: 19.1.2(react@19.1.2) + server-only: + specifier: ^0.0.1 + version: 0.0.1 + stripe: + specifier: ^18.1.0 + version: 18.1.0(@types/node@22.15.18) + swr: + specifier: ^2.3.3 + version: 2.3.3(react@19.1.2) + tailwind-merge: + specifier: ^3.3.0 + version: 3.3.0 + tailwindcss: + specifier: 4.1.7 + version: 4.1.7 + tw-animate-css: + specifier: ^1.3.0 + version: 1.3.0 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + zod: + specifier: ^3.24.4 + version: 3.24.4 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + + '@emnapi/runtime@1.7.1': + resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild/aix-ppc64@0.25.4': + resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.4': + resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.4': + resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.4': + resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.4': + resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.4': + resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.4': + resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.4': + resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.4': + resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.4': + resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.4': + resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.4': + resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.4': + resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.4': + resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.4': + resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.4': + resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.4': + resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.4': + resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.4': + resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.4': + resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.4': + resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.4': + resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.4': + resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.4': + resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.4': + resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@floating-ui/core@1.7.0': + resolution: {integrity: sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==} + + '@floating-ui/dom@1.7.0': + resolution: {integrity: sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==} + + '@floating-ui/react-dom@2.1.2': + resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.9': + resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@next/env@15.5.7': + resolution: {integrity: sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==} + + '@next/swc-darwin-arm64@15.5.7': + resolution: {integrity: sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@15.5.7': + resolution: {integrity: sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@15.5.7': + resolution: {integrity: sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@15.5.7': + resolution: {integrity: sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@15.5.7': + resolution: {integrity: sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@15.5.7': + resolution: {integrity: sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@15.5.7': + resolution: {integrity: sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@15.5.7': + resolution: {integrity: sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@petamoriken/float16@3.9.2': + resolution: {integrity: sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog==} + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.2': + resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} + + '@radix-ui/react-accessible-icon@1.1.7': + resolution: {integrity: sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-accordion@1.2.11': + resolution: {integrity: sha512-l3W5D54emV2ues7jjeG1xcyN7S3jnK3zE2zHqgn0CmMsy9lNJwmgcrmaxS+7ipw15FAivzKNzH3d5EcGoFKw0A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-alert-dialog@1.1.14': + resolution: {integrity: sha512-IOZfZ3nPvN6lXpJTBCunFQPRSvK8MDgSc1FB85xnIpUKOw9en0dJj8JmCAxV7BiZdtYlUpmrQjoTFkVYtdoWzQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-aspect-ratio@1.1.7': + resolution: {integrity: sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.1.10': + resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.3.2': + resolution: {integrity: sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.1.11': + resolution: {integrity: sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context-menu@2.2.15': + resolution: {integrity: sha512-UsQUMjcYTsBjTSXw0P3GO0werEQvUY2plgRQuKoCTtkNr45q1DiL51j4m7gxhABzZ0BadoXNsIbg7F3KwiUBbw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.14': + resolution: {integrity: sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.10': + resolution: {integrity: sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.15': + resolution: {integrity: sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.2': + resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-form@0.1.7': + resolution: {integrity: sha512-IXLKFnaYvFg/KkeV5QfOX7tRnwHXp127koOFUjLWMTrRv5Rny3DQcAtIFFeA/Cli4HHM8DuJCXAUsgnFVJndlw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-hover-card@1.1.14': + resolution: {integrity: sha512-CPYZ24Mhirm+g6D8jArmLzjYu4Eyg3TTUHswR26QgzXBHBe64BO/RHOJKzmF/Dxb4y4f9PKyJdwm/O/AhNkb+Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.7': + resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.15': + resolution: {integrity: sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menubar@1.1.15': + resolution: {integrity: sha512-Z71C7LGD+YDYo3TV81paUs8f3Zbmkvg6VLRQpKYfzioOE6n7fOhA3ApK/V/2Odolxjoc4ENk8AYCjohCNayd5A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-navigation-menu@1.2.13': + resolution: {integrity: sha512-WG8wWfDiJlSF5hELjwfjSGOXcBR/ZMhBFCGYe8vERpC39CQYZeq1PQ2kaYHdye3V95d06H89KGMsVCIE4LWo3g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-one-time-password-field@0.1.7': + resolution: {integrity: sha512-w1vm7AGI8tNXVovOK7TYQHrAGpRF7qQL+ENpT1a743De5Zmay2RbWGKAiYDKIyIuqptns+znCKwNztE2xl1n0Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-password-toggle-field@0.1.2': + resolution: {integrity: sha512-F90uYnlBsLPU1UbSLciLsWQmk8+hdWa6SFw4GXaIdNWxFxI5ITKVdAG64f+Twaa9ic6xE7pqxPyUmodrGjT4pQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.14': + resolution: {integrity: sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.7': + resolution: {integrity: sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.4': + resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-progress@1.1.7': + resolution: {integrity: sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.3.7': + resolution: {integrity: sha512-9w5XhD0KPOrm92OTTE0SysH3sYzHsSTHNvZgUBo/VZ80VdYyB5RneDbc0dKpURS24IxkoFRu/hI0i4XyfFwY6g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.10': + resolution: {integrity: sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.9': + resolution: {integrity: sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.5': + resolution: {integrity: sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.7': + resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slider@1.3.5': + resolution: {integrity: sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.5': + resolution: {integrity: sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.12': + resolution: {integrity: sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toast@1.2.14': + resolution: {integrity: sha512-nAP5FBxBJGQ/YfUB+r+O6USFVkWq3gAInkxyEnmvEV5jtSbfDhfa4hwX8CraCnbjMLsE7XSf/K75l9xXY7joWg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle-group@1.1.10': + resolution: {integrity: sha512-kiU694Km3WFLTC75DdqgM/3Jauf3rD9wxeS9XtyWFKsBUeZA337lC+6uUazT7I1DhanZ5gyD5Stf8uf2dbQxOQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle@1.1.9': + resolution: {integrity: sha512-ZoFkBBz9zv9GWer7wIjvdRxmh2wyc2oKWw6C6CseWd6/yq1DK/l5lJ+wnsmFwJZbBYqr02mrf8A2q/CVCuM3ZA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toolbar@1.1.10': + resolution: {integrity: sha512-jiwQsduEL++M4YBIurjSa+voD86OIytCod0/dbIxFZDLD8NfO1//keXYMfsW8BPcfqwoNjt+y06XcJqAb4KR7A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.7': + resolution: {integrity: sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-is-hydrated@0.1.0': + resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tailwindcss/node@4.1.7': + resolution: {integrity: sha512-9rsOpdY9idRI2NH6CL4wORFY0+Q6fnx9XP9Ju+iq/0wJwGD5IByIgFmwVbyy4ymuyprj8Qh4ErxMKTUL4uNh3g==} + + '@tailwindcss/oxide-android-arm64@4.1.7': + resolution: {integrity: sha512-IWA410JZ8fF7kACus6BrUwY2Z1t1hm0+ZWNEzykKmMNM09wQooOcN/VXr0p/WJdtHZ90PvJf2AIBS/Ceqx1emg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.7': + resolution: {integrity: sha512-81jUw9To7fimGGkuJ2W5h3/oGonTOZKZ8C2ghm/TTxbwvfSiFSDPd6/A/KE2N7Jp4mv3Ps9OFqg2fEKgZFfsvg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.7': + resolution: {integrity: sha512-q77rWjEyGHV4PdDBtrzO0tgBBPlQWKY7wZK0cUok/HaGgbNKecegNxCGikuPJn5wFAlIywC3v+WMBt0PEBtwGw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.7': + resolution: {integrity: sha512-RfmdbbK6G6ptgF4qqbzoxmH+PKfP4KSVs7SRlTwcbRgBwezJkAO3Qta/7gDy10Q2DcUVkKxFLXUQO6J3CRvBGw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.7': + resolution: {integrity: sha512-OZqsGvpwOa13lVd1z6JVwQXadEobmesxQ4AxhrwRiPuE04quvZHWn/LnihMg7/XkN+dTioXp/VMu/p6A5eZP3g==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.7': + resolution: {integrity: sha512-voMvBTnJSfKecJxGkoeAyW/2XRToLZ227LxswLAwKY7YslG/Xkw9/tJNH+3IVh5bdYzYE7DfiaPbRkSHFxY1xA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.7': + resolution: {integrity: sha512-PjGuNNmJeKHnP58M7XyjJyla8LPo+RmwHQpBI+W/OxqrwojyuCQ+GUtygu7jUqTEexejZHr/z3nBc/gTiXBj4A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.7': + resolution: {integrity: sha512-HMs+Va+ZR3gC3mLZE00gXxtBo3JoSQxtu9lobbZd+DmfkIxR54NO7Z+UQNPsa0P/ITn1TevtFxXTpsRU7qEvWg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.7': + resolution: {integrity: sha512-MHZ6jyNlutdHH8rd+YTdr3QbXrHXqwIhHw9e7yXEBcQdluGwhpQY2Eku8UZK6ReLaWtQ4gijIv5QoM5eE+qlsA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.7': + resolution: {integrity: sha512-ANaSKt74ZRzE2TvJmUcbFQ8zS201cIPxUDm5qez5rLEwWkie2SkGtA4P+GPTj+u8N6JbPrC8MtY8RmJA35Oo+A==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.7': + resolution: {integrity: sha512-HUiSiXQ9gLJBAPCMVRk2RT1ZrBjto7WvqsPBwUrNK2BcdSxMnk19h4pjZjI7zgPhDxlAbJSumTC4ljeA9y0tEw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.7': + resolution: {integrity: sha512-rYHGmvoHiLJ8hWucSfSOEmdCBIGZIq7SpkPRSqLsH2Ab2YUNgKeAPT1Fi2cx3+hnYOrAb0jp9cRyode3bBW4mQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.7': + resolution: {integrity: sha512-5SF95Ctm9DFiUyjUPnDGkoKItPX/k+xifcQhcqX5RA85m50jw1pT/KzjdvlqxRja45Y52nR4MR9fD1JYd7f8NQ==} + engines: {node: '>= 10'} + + '@tailwindcss/postcss@4.1.7': + resolution: {integrity: sha512-88g3qmNZn7jDgrrcp3ZXEQfp9CVox7xjP1HN2TFKI03CltPVd/c61ydn5qJJL8FYunn0OqBaW5HNUga0kmPVvw==} + + '@types/node@22.15.18': + resolution: {integrity: sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==} + + '@types/react-dom@19.1.5': + resolution: {integrity: sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==} + peerDependencies: + '@types/react': ^19.0.0 + + '@types/react@19.1.4': + resolution: {integrity: sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==} + + aria-hidden@1.2.4: + resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} + engines: {node: '>=10'} + + autoprefixer@10.4.21: + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + bcryptjs@3.0.2: + resolution: {integrity: sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==} + hasBin: true + + browserslist@4.24.5: + resolution: {integrity: sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + caniuse-lite@1.0.30001718: + resolution: {integrity: sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + dotenv@16.5.0: + resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} + engines: {node: '>=12'} + + drizzle-kit@0.31.1: + resolution: {integrity: sha512-PUjYKWtzOzPtdtQlTHQG3qfv4Y0XT8+Eas6UbxCmxTj7qgMf+39dDujf1BP1I+qqZtw9uzwTh8jYtkMuCq+B0Q==} + hasBin: true + + drizzle-orm@0.43.1: + resolution: {integrity: sha512-dUcDaZtE/zN4RV/xqGrVSMpnEczxd5cIaoDeor7Zst9wOe/HzC/7eAaulywWGYXdDEc9oBPMjayVEDg0ziTLJA==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1.13' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.155: + resolution: {integrity: sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==} + + enhanced-resolve@5.18.1: + resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + engines: {node: '>=10.13.0'} + + env-paths@3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.4: + resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gel@2.1.0: + resolution: {integrity: sha512-HCeRqInCt6BjbMmeghJ6BKeYwOj7WJT5Db6IWWAA3IMUUa7or7zJfTUEkUWCxiOtoXnwnm96sFK9Fr47Yh2hOA==} + engines: {node: '>= 18.0.0'} + hasBin: true + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.10.0: + resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + isexe@3.1.1: + resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} + engines: {node: '>=16'} + + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + + jose@6.0.11: + resolution: {integrity: sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==} + + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.1: + resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + engines: {node: '>= 12.0.0'} + + lucide-react@0.511.0: + resolution: {integrity: sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.0.2: + resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} + engines: {node: '>= 18'} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + next@15.5.7: + resolution: {integrity: sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.3: + resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} + engines: {node: ^10 || ^12 || >=14} + + postgres@3.4.5: + resolution: {integrity: sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg==} + engines: {node: '>=12'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + radix-ui@1.4.2: + resolution: {integrity: sha512-fT/3YFPJzf2WUpqDoQi005GS8EpCi+53VhcLaHUj5fwkPYiZAjk1mSxFvbMA8Uq71L03n+WysuYC+mlKkXxt/Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + react-dom@19.1.2: + resolution: {integrity: sha512-dEoydsCp50i7kS1xHOmPXq4zQYoGWedUsvqv9H6zdif2r7yLHygyfP9qou71TulRN0d6ng9EbRVsQhSqfUc19g==} + peerDependencies: + react: ^19.1.2 + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.6.3: + resolution: {integrity: sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react@19.1.2: + resolution: {integrity: sha512-MdWVitvLbQULD+4DP8GYjZUrepGW7d+GQkNVqJEzNxE+e9WIa4egVFE/RDfVb1u9u/Jw7dNMmPB4IqxzbFYJ0w==} + engines: {node: '>=0.10.0'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + server-only@0.0.1: + resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shell-quote@1.8.2: + resolution: {integrity: sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==} + engines: {node: '>= 0.4'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + stripe@18.1.0: + resolution: {integrity: sha512-MLDiniPTHqcfIT3anyBPmOEcaiDhYa7/jRaNypQ3Rt2SJnayQZBvVbFghIziUCZdltGAndm/ZxVOSw6uuSCDig==} + engines: {node: '>=12.*'} + peerDependencies: + '@types/node': '>=12.x.x' + peerDependenciesMeta: + '@types/node': + optional: true + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + swr@2.3.3: + resolution: {integrity: sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + tailwind-merge@3.3.0: + resolution: {integrity: sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==} + + tailwindcss@4.1.7: + resolution: {integrity: sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg==} + + tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + + tar@7.4.3: + resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + engines: {node: '>=18'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tw-animate-css@1.3.0: + resolution: {integrity: sha512-jrJ0XenzS9KVuDThJDvnhalbl4IYiMQ/XvpA0a2FL8KmlK+6CSMviO7ROY/I7z1NnUs5NnDhlM6fXmF40xPxzw==} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + zod@3.24.4: + resolution: {integrity: sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@drizzle-team/brocli@0.10.2': {} + + '@emnapi/runtime@1.7.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.10.0 + + '@esbuild/aix-ppc64@0.25.4': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.25.4': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.25.4': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.25.4': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.25.4': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.25.4': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.25.4': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.25.4': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.25.4': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.25.4': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.25.4': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.25.4': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.25.4': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.25.4': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.25.4': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.25.4': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.25.4': + optional: true + + '@esbuild/netbsd-arm64@0.25.4': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.25.4': + optional: true + + '@esbuild/openbsd-arm64@0.25.4': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.25.4': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.25.4': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.25.4': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.25.4': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.25.4': + optional: true + + '@floating-ui/core@1.7.0': + dependencies: + '@floating-ui/utils': 0.2.9 + + '@floating-ui/dom@1.7.0': + dependencies: + '@floating-ui/core': 1.7.0 + '@floating-ui/utils': 0.2.9 + + '@floating-ui/react-dom@2.1.2(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@floating-ui/dom': 1.7.0 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + + '@floating-ui/utils@0.2.9': {} + + '@img/colour@1.0.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.7.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@jridgewell/gen-mapping@0.3.8': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@next/env@15.5.7': {} + + '@next/swc-darwin-arm64@15.5.7': + optional: true + + '@next/swc-darwin-x64@15.5.7': + optional: true + + '@next/swc-linux-arm64-gnu@15.5.7': + optional: true + + '@next/swc-linux-arm64-musl@15.5.7': + optional: true + + '@next/swc-linux-x64-gnu@15.5.7': + optional: true + + '@next/swc-linux-x64-musl@15.5.7': + optional: true + + '@next/swc-win32-arm64-msvc@15.5.7': + optional: true + + '@next/swc-win32-x64-msvc@15.5.7': + optional: true + + '@petamoriken/float16@3.9.2': + optional: true + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.2': {} + + '@radix-ui/react-accessible-icon@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-accordion@1.2.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collapsible': 1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-alert-dialog@1.1.14(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-dialog': 1.1.14(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-aspect-ratio@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-avatar@1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-checkbox@1.3.2(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-collapsible@1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.4)(react@19.1.2)': + dependencies: + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-context-menu@2.2.15(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-menu': 2.1.15(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-context@1.1.2(@types/react@19.1.4)(react@19.1.2)': + dependencies: + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-dialog@1.1.14(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + aria-hidden: 1.2.4 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + react-remove-scroll: 2.6.3(@types/react@19.1.4)(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-direction@1.1.1(@types/react@19.1.4)(react@19.1.2)': + dependencies: + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-dropdown-menu@2.1.15(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-menu': 2.1.15(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-focus-guards@1.1.2(@types/react@19.1.4)(react@19.1.2)': + dependencies: + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-form@0.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-hover-card@1.1.14(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-id@1.1.1(@types/react@19.1.4)(react@19.1.2)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-label@2.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-menu@2.1.15(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + aria-hidden: 1.2.4 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + react-remove-scroll: 2.6.3(@types/react@19.1.4)(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-menubar@1.1.15(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-menu': 2.1.15(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-navigation-menu@1.2.13(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-one-time-password-field@0.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-password-toggle-field@0.1.2(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-popover@1.1.14(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + aria-hidden: 1.2.4 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + react-remove-scroll: 2.6.3(@types/react@19.1.4)(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-popper@1.2.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@floating-ui/react-dom': 2.1.2(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/rect': 1.1.1 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-presence@1.1.4(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-progress@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-radio-group@1.3.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-scroll-area@1.2.9(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-select@2.2.5(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + aria-hidden: 1.2.4 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + react-remove-scroll: 2.6.3(@types/react@19.1.4)(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-separator@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-slider@1.3.5(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-slot@1.2.3(@types/react@19.1.4)(react@19.1.2)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-switch@1.2.5(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-tabs@1.1.12(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-toast@1.2.14(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-toggle-group@1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-toggle': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-toggle@1.1.9(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-toolbar@1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-toggle-group': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-tooltip@1.2.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.4)(react@19.1.2)': + dependencies: + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.1.4)(react@19.1.2)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.1.4)(react@19.1.2)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.1.4)(react@19.1.2)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.1.4)(react@19.1.2)': + dependencies: + react: 19.1.2 + use-sync-external-store: 1.5.0(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.4)(react@19.1.2)': + dependencies: + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.1.4)(react@19.1.2)': + dependencies: + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.1.4)(react@19.1.2)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.1.4)(react@19.1.2)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/rect@1.1.1': {} + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.1.7': + dependencies: + '@ampproject/remapping': 2.3.0 + enhanced-resolve: 5.18.1 + jiti: 2.4.2 + lightningcss: 1.30.1 + magic-string: 0.30.17 + source-map-js: 1.2.1 + tailwindcss: 4.1.7 + + '@tailwindcss/oxide-android-arm64@4.1.7': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.7': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.7': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.7': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.7': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.7': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.7': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.7': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.7': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.7': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.7': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.7': + optional: true + + '@tailwindcss/oxide@4.1.7': + dependencies: + detect-libc: 2.0.4 + tar: 7.4.3 + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.7 + '@tailwindcss/oxide-darwin-arm64': 4.1.7 + '@tailwindcss/oxide-darwin-x64': 4.1.7 + '@tailwindcss/oxide-freebsd-x64': 4.1.7 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.7 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.7 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.7 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.7 + '@tailwindcss/oxide-linux-x64-musl': 4.1.7 + '@tailwindcss/oxide-wasm32-wasi': 4.1.7 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.7 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.7 + + '@tailwindcss/postcss@4.1.7': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.1.7 + '@tailwindcss/oxide': 4.1.7 + postcss: 8.5.3 + tailwindcss: 4.1.7 + + '@types/node@22.15.18': + dependencies: + undici-types: 6.21.0 + + '@types/react-dom@19.1.5(@types/react@19.1.4)': + dependencies: + '@types/react': 19.1.4 + + '@types/react@19.1.4': + dependencies: + csstype: 3.1.3 + + aria-hidden@1.2.4: + dependencies: + tslib: 2.8.1 + + autoprefixer@10.4.21(postcss@8.5.3): + dependencies: + browserslist: 4.24.5 + caniuse-lite: 1.0.30001718 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + bcryptjs@3.0.2: {} + + browserslist@4.24.5: + dependencies: + caniuse-lite: 1.0.30001718 + electron-to-chromium: 1.5.155 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.24.5) + + buffer-from@1.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + caniuse-lite@1.0.30001718: {} + + chownr@3.0.0: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + client-only@0.0.1: {} + + clsx@2.1.1: {} + + csstype@3.1.3: {} + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + dequal@2.0.3: {} + + detect-libc@2.0.4: {} + + detect-libc@2.1.2: + optional: true + + detect-node-es@1.1.0: {} + + dotenv@16.5.0: {} + + drizzle-kit@0.31.1: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.25.4 + esbuild-register: 3.6.0(esbuild@0.25.4) + transitivePeerDependencies: + - supports-color + + drizzle-orm@0.43.1(gel@2.1.0)(postgres@3.4.5): + optionalDependencies: + gel: 2.1.0 + postgres: 3.4.5 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.155: {} + + enhanced-resolve@5.18.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + + env-paths@3.0.0: + optional: true + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + esbuild-register@3.6.0(esbuild@0.25.4): + dependencies: + debug: 4.4.1 + esbuild: 0.25.4 + transitivePeerDependencies: + - supports-color + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.25.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.4 + '@esbuild/android-arm': 0.25.4 + '@esbuild/android-arm64': 0.25.4 + '@esbuild/android-x64': 0.25.4 + '@esbuild/darwin-arm64': 0.25.4 + '@esbuild/darwin-x64': 0.25.4 + '@esbuild/freebsd-arm64': 0.25.4 + '@esbuild/freebsd-x64': 0.25.4 + '@esbuild/linux-arm': 0.25.4 + '@esbuild/linux-arm64': 0.25.4 + '@esbuild/linux-ia32': 0.25.4 + '@esbuild/linux-loong64': 0.25.4 + '@esbuild/linux-mips64el': 0.25.4 + '@esbuild/linux-ppc64': 0.25.4 + '@esbuild/linux-riscv64': 0.25.4 + '@esbuild/linux-s390x': 0.25.4 + '@esbuild/linux-x64': 0.25.4 + '@esbuild/netbsd-arm64': 0.25.4 + '@esbuild/netbsd-x64': 0.25.4 + '@esbuild/openbsd-arm64': 0.25.4 + '@esbuild/openbsd-x64': 0.25.4 + '@esbuild/sunos-x64': 0.25.4 + '@esbuild/win32-arm64': 0.25.4 + '@esbuild/win32-ia32': 0.25.4 + '@esbuild/win32-x64': 0.25.4 + + escalade@3.2.0: {} + + fraction.js@4.3.7: {} + + function-bind@1.1.2: {} + + gel@2.1.0: + dependencies: + '@petamoriken/float16': 3.9.2 + debug: 4.4.1 + env-paths: 3.0.0 + semver: 7.7.3 + shell-quote: 1.8.2 + which: 4.0.0 + transitivePeerDependencies: + - supports-color + optional: true + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-nonce@1.0.1: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-tsconfig@4.10.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + isexe@3.1.1: + optional: true + + jiti@2.4.2: {} + + jose@6.0.11: {} + + lightningcss-darwin-arm64@1.30.1: + optional: true + + lightningcss-darwin-x64@1.30.1: + optional: true + + lightningcss-freebsd-x64@1.30.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.1: + optional: true + + lightningcss-linux-arm64-gnu@1.30.1: + optional: true + + lightningcss-linux-arm64-musl@1.30.1: + optional: true + + lightningcss-linux-x64-gnu@1.30.1: + optional: true + + lightningcss-linux-x64-musl@1.30.1: + optional: true + + lightningcss-win32-arm64-msvc@1.30.1: + optional: true + + lightningcss-win32-x64-msvc@1.30.1: + optional: true + + lightningcss@1.30.1: + dependencies: + detect-libc: 2.0.4 + optionalDependencies: + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + lightningcss-win32-arm64-msvc: 1.30.1 + lightningcss-win32-x64-msvc: 1.30.1 + + lucide-react@0.511.0(react@19.1.2): + dependencies: + react: 19.1.2 + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + math-intrinsics@1.1.0: {} + + minipass@7.1.2: {} + + minizlib@3.0.2: + dependencies: + minipass: 7.1.2 + + mkdirp@3.0.1: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + next@15.5.7(react-dom@19.1.2(react@19.1.2))(react@19.1.2): + dependencies: + '@next/env': 15.5.7 + '@swc/helpers': 0.5.15 + caniuse-lite: 1.0.30001718 + postcss: 8.4.31 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + styled-jsx: 5.1.6(react@19.1.2) + optionalDependencies: + '@next/swc-darwin-arm64': 15.5.7 + '@next/swc-darwin-x64': 15.5.7 + '@next/swc-linux-arm64-gnu': 15.5.7 + '@next/swc-linux-arm64-musl': 15.5.7 + '@next/swc-linux-x64-gnu': 15.5.7 + '@next/swc-linux-x64-musl': 15.5.7 + '@next/swc-win32-arm64-msvc': 15.5.7 + '@next/swc-win32-x64-msvc': 15.5.7 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + node-releases@2.0.19: {} + + normalize-range@0.1.2: {} + + object-inspect@1.13.4: {} + + picocolors@1.1.1: {} + + postcss-value-parser@4.2.0: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.3: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres@3.4.5: {} + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + radix-ui@1.4.2(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2): + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-accessible-icon': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-accordion': 1.2.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-alert-dialog': 1.1.14(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-aspect-ratio': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-avatar': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-checkbox': 1.3.2(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-collapsible': 1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context-menu': 2.2.15(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-dialog': 1.1.14(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-dropdown-menu': 2.1.15(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-form': 0.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-hover-card': 1.1.14(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-menu': 2.1.15(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-menubar': 1.1.15(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-navigation-menu': 1.2.13(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-one-time-password-field': 0.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-password-toggle-field': 0.1.2(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-popover': 1.1.14(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-progress': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-radio-group': 1.3.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-scroll-area': 1.2.9(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-select': 2.2.5(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slider': 1.3.5(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-switch': 1.2.5(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-tabs': 1.1.12(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-toast': 1.2.14(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-toggle': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-toggle-group': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-toolbar': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-tooltip': 1.2.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + react-dom@19.1.2(react@19.1.2): + dependencies: + react: 19.1.2 + scheduler: 0.26.0 + + react-remove-scroll-bar@2.3.8(@types/react@19.1.4)(react@19.1.2): + dependencies: + react: 19.1.2 + react-style-singleton: 2.2.3(@types/react@19.1.4)(react@19.1.2) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.4 + + react-remove-scroll@2.6.3(@types/react@19.1.4)(react@19.1.2): + dependencies: + react: 19.1.2 + react-remove-scroll-bar: 2.3.8(@types/react@19.1.4)(react@19.1.2) + react-style-singleton: 2.2.3(@types/react@19.1.4)(react@19.1.2) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.1.4)(react@19.1.2) + use-sidecar: 1.1.3(@types/react@19.1.4)(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + + react-style-singleton@2.2.3(@types/react@19.1.4)(react@19.1.2): + dependencies: + get-nonce: 1.0.1 + react: 19.1.2 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.4 + + react@19.1.2: {} + + resolve-pkg-maps@1.0.0: {} + + scheduler@0.26.0: {} + + semver@7.7.3: + optional: true + + server-only@0.0.1: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + optional: true + + shell-quote@1.8.2: + optional: true + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + stripe@18.1.0(@types/node@22.15.18): + dependencies: + qs: 6.14.0 + optionalDependencies: + '@types/node': 22.15.18 + + styled-jsx@5.1.6(react@19.1.2): + dependencies: + client-only: 0.0.1 + react: 19.1.2 + + swr@2.3.3(react@19.1.2): + dependencies: + dequal: 2.0.3 + react: 19.1.2 + use-sync-external-store: 1.5.0(react@19.1.2) + + tailwind-merge@3.3.0: {} + + tailwindcss@4.1.7: {} + + tapable@2.2.1: {} + + tar@7.4.3: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.0.2 + mkdirp: 3.0.1 + yallist: 5.0.0 + + tslib@2.8.1: {} + + tw-animate-css@1.3.0: {} + + typescript@5.8.3: {} + + undici-types@6.21.0: {} + + update-browserslist-db@1.1.3(browserslist@4.24.5): + dependencies: + browserslist: 4.24.5 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-callback-ref@1.3.3(@types/react@19.1.4)(react@19.1.2): + dependencies: + react: 19.1.2 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.4 + + use-sidecar@1.1.3(@types/react@19.1.4)(react@19.1.2): + dependencies: + detect-node-es: 1.1.0 + react: 19.1.2 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.4 + + use-sync-external-store@1.5.0(react@19.1.2): + dependencies: + react: 19.1.2 + + which@4.0.0: + dependencies: + isexe: 3.1.1 + optional: true + + yallist@5.0.0: {} + + zod@3.24.4: {} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/postcss.config.mjs b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/postcss.config.mjs new file mode 100644 index 000000000..a34a3d560 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + }, +}; diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-saas/tsconfig.json b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/tsconfig.json new file mode 100644 index 000000000..f969d125d --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-saas/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "baseUrl": ".", + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-todo/.gitignore b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/.gitignore new file mode 100644 index 000000000..243a7b92e --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/.gitignore @@ -0,0 +1,34 @@ +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-todo/README.md b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/README.md new file mode 100644 index 000000000..299b5b06e --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/README.md @@ -0,0 +1,104 @@ +# Next.js 15 Todo App + +A simple, full-stack todo application built with Next.js 15, utilizing server-side API routes with in-memory storage for managing todo items. + +## Features + +- **Full CRUD Operations**: Create, Read, Update, and Delete todos +- **Server-Side API Routes**: RESTful API endpoints using Next.js 15 route handlers +- **In-Memory Storage**: Simple data persistence (resets on server restart) +- **Modern UI**: Clean interface with Tailwind CSS and custom components +- **Type Safety**: Full TypeScript support with Zod validation + +## Tech Stack + +- **Next.js 15.3** - React framework with App Router +- **React 19** - UI library +- **TypeScript** - Type safety +- **Tailwind CSS 4** - Styling +- **Zod** - Schema validation +- **Lucide React** - Icons + +## Getting Started + +### Prerequisites + +- Node.js 18+ and pnpm + +### Installation + +1. Install dependencies: +```bash +pnpm install +``` + +2. Start the development server: +```bash +pnpm dev +``` + +Open [http://localhost:3000](http://localhost:3000) to see the app. + +## API Routes + +### GET /api/todos +Get all todos + +### POST /api/todos +Create a new todo +```json +{ + "title": "Learn Next.js", + "description": "Study Next.js 15 features", + "completed": false +} +``` + +### GET /api/todos/[id] +Get a specific todo by ID + +### PATCH /api/todos/[id] +Update a todo +```json +{ + "completed": true +} +``` + +### DELETE /api/todos/[id] +Delete a todo + +## Project Structure + +``` +apps/next-js/15-todo/ +├── app/ +│ ├── api/ +│ │ └── todos/ +│ │ ├── route.ts # GET, POST /api/todos +│ │ └── [id]/ +│ │ └── route.ts # GET, PATCH, DELETE /api/todos/[id] +│ ├── layout.tsx +│ ├── page.tsx +│ └── globals.css +├── components/ +│ ├── todos/ +│ │ ├── todo-list.tsx # Main todo list component +│ │ ├── todo-item.tsx # Individual todo item +│ │ └── todo-form.tsx # Form for adding todos +│ └── ui/ # Reusable UI components +├── lib/ +│ ├── data.ts # In-memory data store +│ └── utils.ts +└── package.json +``` + +## Notes + +- Data is stored in memory and will be reset when the development server restarts +- The app comes pre-seeded with 3 sample todos +- Perfect for learning Next.js 15 API routes without database complexity + +## License + +MIT diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-todo/app/about/page.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/app/about/page.tsx new file mode 100644 index 000000000..f59817800 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/app/about/page.tsx @@ -0,0 +1,101 @@ +import Link from 'next/link'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; + +export const metadata = { + title: 'About - Todo App', + description: 'Learn more about this Next.js 15 todo application', +}; + +export default function AboutPage() { + return ( +
+
+

About This App

+

+ A demonstration of Next.js 15 with the App Router +

+
+ + + + What is this? + A full-stack todo application showcase + + +

+ This is a simple todo application built with Next.js 15 using the App Router architecture. + It demonstrates modern web development practices including: +

+
    +
  • Server-side API routes for data management
  • +
  • Client-side state management with React hooks
  • +
  • Type-safe development with TypeScript
  • +
  • Input validation using Zod
  • +
  • Modern UI with Tailwind CSS
  • +
  • Responsive design patterns
  • +
+
+
+ + + + Tech Stack + Built with modern web technologies + + +
+
+

Framework

+

Next.js 15.3

+
+
+

UI Library

+

React 19

+
+
+

Language

+

TypeScript

+
+
+

Styling

+

Tailwind CSS 4

+
+
+

Validation

+

Zod

+
+
+

Icons

+

Lucide React

+
+
+
+
+ + + + Features + What you can do with this app + + +
    +
  • Create new todo items with titles and descriptions
  • +
  • Mark todos as completed or incomplete
  • +
  • Delete todos you no longer need
  • +
  • View active and completed todos separately
  • +
  • All data persists in memory during the session
  • +
+
+
+ +
+ + Back to Todos + +
+
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-todo/app/api/todos/[id]/route.ts b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/app/api/todos/[id]/route.ts new file mode 100644 index 000000000..66a4e9b61 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/app/api/todos/[id]/route.ts @@ -0,0 +1,105 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getTodoById, updateTodo, deleteTodo } from '@/lib/data'; +import { z } from 'zod'; + +const updateTodoSchema = z.object({ + title: z.string().min(1).max(255).optional(), + description: z.string().optional(), + completed: z.boolean().optional(), +}); + +// GET /api/todos/[id] - Get a specific todo +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const todoId = parseInt(id); + + if (isNaN(todoId)) { + return NextResponse.json({ error: 'Invalid todo ID' }, { status: 400 }); + } + + const todo = getTodoById(todoId); + + if (!todo) { + return NextResponse.json({ error: 'Todo not found' }, { status: 404 }); + } + + return NextResponse.json(todo); + } catch (error) { + console.error('Error fetching todo:', error); + return NextResponse.json( + { error: 'Failed to fetch todo' }, + { status: 500 } + ); + } +} + +// PATCH /api/todos/[id] - Update a todo +export async function PATCH( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const todoId = parseInt(id); + + if (isNaN(todoId)) { + return NextResponse.json({ error: 'Invalid todo ID' }, { status: 400 }); + } + + const body = await request.json(); + const validatedData = updateTodoSchema.parse(body); + + const updatedTodo = updateTodo(todoId, validatedData); + + if (!updatedTodo) { + return NextResponse.json({ error: 'Todo not found' }, { status: 404 }); + } + + return NextResponse.json(updatedTodo); + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: 'Invalid todo data', details: error.errors }, + { status: 400 } + ); + } + console.error('Error updating todo:', error); + return NextResponse.json( + { error: 'Failed to update todo' }, + { status: 500 } + ); + } +} + +// DELETE /api/todos/[id] - Delete a todo +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const todoId = parseInt(id); + + if (isNaN(todoId)) { + return NextResponse.json({ error: 'Invalid todo ID' }, { status: 400 }); + } + + const deleted = deleteTodo(todoId); + + if (!deleted) { + return NextResponse.json({ error: 'Todo not found' }, { status: 404 }); + } + + return NextResponse.json({ message: 'Todo deleted successfully' }); + } catch (error) { + console.error('Error deleting todo:', error); + return NextResponse.json( + { error: 'Failed to delete todo' }, + { status: 500 } + ); + } +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-todo/app/api/todos/route.ts b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/app/api/todos/route.ts new file mode 100644 index 000000000..9b37b3704 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/app/api/todos/route.ts @@ -0,0 +1,51 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getTodos, createTodo } from '@/lib/data'; +import { z } from 'zod'; + +const todoSchema = z.object({ + title: z.string().min(1).max(255), + description: z.string().optional(), + completed: z.boolean().optional(), +}); + +// GET /api/todos - Get all todos +export async function GET() { + try { + const allTodos = getTodos(); + return NextResponse.json(allTodos); + } catch (error) { + console.error('Error fetching todos:', error); + return NextResponse.json( + { error: 'Failed to fetch todos' }, + { status: 500 } + ); + } +} + +// POST /api/todos - Create a new todo +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const validatedData = todoSchema.parse(body); + + const newTodo = createTodo({ + title: validatedData.title, + description: validatedData.description, + completed: validatedData.completed, + }); + + return NextResponse.json(newTodo, { status: 201 }); + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: 'Invalid todo data', details: error.errors }, + { status: 400 } + ); + } + console.error('Error creating todo:', error); + return NextResponse.json( + { error: 'Failed to create todo' }, + { status: 500 } + ); + } +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-todo/app/globals.css b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/app/globals.css new file mode 100644 index 000000000..80ef41b69 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/app/globals.css @@ -0,0 +1,57 @@ +@import 'tailwindcss'; + +@theme { + --color-primary: #2563eb; + --color-primary-foreground: #ffffff; + --color-secondary: #f1f5f9; + --color-secondary-foreground: #0f172a; + --color-destructive: #ef4444; + --color-destructive-foreground: #ffffff; + --color-muted: #f1f5f9; + --color-muted-foreground: #64748b; + --color-accent: #f1f5f9; + --color-accent-foreground: #0f172a; + --color-card: #ffffff; + --color-card-foreground: #0f172a; + --color-popover: #ffffff; + --color-popover-foreground: #0f172a; + --color-border: #e2e8f0; + --color-input: #e2e8f0; + --color-ring: #2563eb; + --color-background: #ffffff; + --color-foreground: #0f172a; + --radius: 0.5rem; +} + +@media (prefers-color-scheme: dark) { + @theme { + --color-primary: #3b82f6; + --color-primary-foreground: #ffffff; + --color-secondary: #1e293b; + --color-secondary-foreground: #f8fafc; + --color-destructive: #dc2626; + --color-destructive-foreground: #ffffff; + --color-muted: #1e293b; + --color-muted-foreground: #94a3b8; + --color-accent: #1e293b; + --color-accent-foreground: #f8fafc; + --color-card: #0f172a; + --color-card-foreground: #f8fafc; + --color-popover: #0f172a; + --color-popover-foreground: #f8fafc; + --color-border: #334155; + --color-input: #334155; + --color-ring: #3b82f6; + --color-background: #020817; + --color-foreground: #f8fafc; + } +} + +* { + border-color: var(--color-border); +} + +body { + color: var(--color-foreground); + background: var(--color-background); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-todo/app/layout.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/app/layout.tsx new file mode 100644 index 000000000..451bba025 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/app/layout.tsx @@ -0,0 +1,19 @@ +import type { Metadata } from 'next'; +import './globals.css'; + +export const metadata: Metadata = { + title: 'Todo App - Next.js 15', + description: 'A full-stack todo application built with Next.js 15', +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-todo/app/page.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/app/page.tsx new file mode 100644 index 000000000..25433584f --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/app/page.tsx @@ -0,0 +1,5 @@ +import { TodoList } from '@/components/todos/todo-list'; + +export default function HomePage() { + return ; +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-todo/components.json b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/components.json new file mode 100644 index 000000000..2739f5df6 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-todo/components/todos/todo-form.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/components/todos/todo-form.tsx new file mode 100644 index 000000000..2d9362784 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/components/todos/todo-form.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; + +interface TodoFormProps { + onAdd: (title: string, description: string) => void; +} + +export function TodoForm({ onAdd }: TodoFormProps) { + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (title.trim()) { + onAdd(title, description); + setTitle(''); + setDescription(''); + } + }; + + return ( +
+
+ setTitle(e.target.value)} + className="w-full" + /> +
+
+ setDescription(e.target.value)} + className="w-full" + /> +
+ +
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-todo/components/todos/todo-item.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/components/todos/todo-item.tsx new file mode 100644 index 000000000..49c20fb89 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/components/todos/todo-item.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { Todo } from '@/lib/data'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Trash2 } from 'lucide-react'; + +interface TodoItemProps { + todo: Todo; + onToggle: (id: number, completed: boolean) => void; + onDelete: (id: number) => void; +} + +export function TodoItem({ todo, onToggle, onDelete }: TodoItemProps) { + return ( + + +
+
+ onToggle(todo.id, e.target.checked)} + className="mt-1" + /> +
+ + {todo.title} + + {todo.description && ( + + {todo.description} + + )} +
+
+ +
+
+
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-todo/components/todos/todo-list.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/components/todos/todo-list.tsx new file mode 100644 index 000000000..9bad4230f --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/components/todos/todo-list.tsx @@ -0,0 +1,165 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { Todo } from '@/lib/data'; +import { TodoForm } from './todo-form'; +import { TodoItem } from './todo-item'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; + +export function TodoList() { + const [todos, setTodos] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetchTodos(); + }, []); + + const fetchTodos = async () => { + try { + const response = await fetch('/api/todos'); + if (response.ok) { + const data = await response.json(); + setTodos(data); + } + } catch (error) { + console.error('Failed to fetch todos:', error); + } finally { + setLoading(false); + } + }; + + const handleAddTodo = async (title: string, description: string) => { + try { + const response = await fetch('/api/todos', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ title, description }), + }); + + if (response.ok) { + const newTodo = await response.json(); + setTodos([...todos, newTodo]); + } + } catch (error) { + console.error('Failed to add todo:', error); + } + }; + + const handleToggleTodo = async (id: number, completed: boolean) => { + try { + const response = await fetch(`/api/todos/${id}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ completed }), + }); + + if (response.ok) { + const updatedTodo = await response.json(); + setTodos(todos.map((todo) => (todo.id === id ? updatedTodo : todo))); + } + } catch (error) { + console.error('Failed to update todo:', error); + } + }; + + const handleDeleteTodo = async (id: number) => { + try { + const response = await fetch(`/api/todos/${id}`, { + method: 'DELETE', + }); + + if (response.ok) { + setTodos(todos.filter((todo) => todo.id !== id)); + } + } catch (error) { + console.error('Failed to delete todo:', error); + } + }; + + if (loading) { + return ( +
+

Loading...

+
+ ); + } + + const activeTodos = todos.filter((todo) => !todo.completed); + const completedTodos = todos.filter((todo) => todo.completed); + + return ( +
+
+
+

Todo App

+ + About + +
+

+ Manage your tasks with Next.js 15 server-side API routes +

+
+ + + + Add New Todo + Create a new task to keep track of + + + + + + +
+
+

+ Active Tasks ({activeTodos.length}) +

+ {activeTodos.length === 0 ? ( +

+ No active tasks. Add one above! +

+ ) : ( +
+ {activeTodos.map((todo) => ( + + ))} +
+ )} +
+ + {completedTodos.length > 0 && ( +
+

+ Completed ({completedTodos.length}) +

+
+ {completedTodos.map((todo) => ( + + ))} +
+
+ )} +
+
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-app-router-todo/components/ui/button.tsx b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/components/ui/button.tsx new file mode 100644 index 000000000..5210bd6e9 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-app-router-todo/components/ui/button.tsx @@ -0,0 +1,54 @@ +import * as React from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { cn } from '@/lib/utils'; + +const buttonVariants = cva( + 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', + { + variants: { + variant: { + default: + 'bg-primary text-primary-foreground shadow hover:bg-primary/90', + destructive: + 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', + outline: + 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', + secondary: + 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-9 px-4 py-2', + sm: 'h-8 rounded-md px-3 text-xs', + lg: 'h-10 rounded-md px-8', + icon: 'h-9 w-9', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, ...props }, ref) => { + return ( + +
+ +
+ {/* Sidebar */} + + + {/* Main content */} +
{children}
+
+
+
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/header.tsx b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/header.tsx new file mode 100644 index 000000000..f9a68851f --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/header.tsx @@ -0,0 +1,105 @@ +'use client'; + +import Link from 'next/link'; +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { CircleIcon, Home, LogOut } from 'lucide-react'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { useRouter } from 'next/router'; +import { User } from '@/lib/db/schema'; +import useSWR, { mutate } from 'swr'; + +const fetcher = (url: string) => fetch(url).then((res) => res.json()); + +function UserMenu() { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const { data: user } = useSWR('/api/user', fetcher); + const router = useRouter(); + + async function handleSignOut() { + try { + // Call sign-out API to delete HttpOnly session cookie + await fetch('/api/auth/sign-out', { + method: 'POST' + }); + + // Clear SWR cache + mutate('/api/user', null, false); + mutate('/api/team', null, false); + + // Redirect to home + router.push('/'); + } catch (error) { + console.error('Sign out error:', error); + } + } + + if (!user) { + return ( + <> + + Pricing + + + + ); + } + + return ( + + + + + + {user.email + .split(' ') + .map((n) => n[0]) + .join('')} + + + + + + + + Dashboard + + + + + Sign out + + + + ); +} + +export function Header() { + return ( +
+
+ + + ACME + +
+ +
+
+
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/login.tsx b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/login.tsx new file mode 100644 index 000000000..325f0cad5 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/login.tsx @@ -0,0 +1,193 @@ +'use client'; + +import Link from 'next/link'; +import { useState, useTransition } from 'react'; +import { useRouter } from 'next/router'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { CircleIcon, Loader2 } from 'lucide-react'; + +export function Login({ + mode = 'signin', + redirect, + priceId, + inviteId +}: { + mode?: 'signin' | 'signup'; + redirect?: string | null; + priceId?: string | null; + inviteId?: string | null; +}) { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + const [error, setError] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(''); + + const formData = new FormData(e.currentTarget); + const data = { + email: formData.get('email') as string, + password: formData.get('password') as string, + redirect: formData.get('redirect') as string, + priceId: formData.get('priceId') as string, + inviteId: formData.get('inviteId') as string + }; + + startTransition(async () => { + try { + const response = await fetch(`/api/auth/${mode === 'signin' ? 'sign-in' : 'sign-up'}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + + const result = await response.json(); + + if (!response.ok) { + setError(result.error || 'An error occurred'); + setEmail(result.email || data.email); + setPassword(result.password || data.password); + return; + } + + if (result.success && result.redirectTo) { + router.push(result.redirectTo); + } else if (result.url) { + // Stripe checkout redirect + window.location.href = result.url; + } + } catch (err) { + setError('An unexpected error occurred. Please try again.'); + } + }); + } + + return ( +
+
+
+ +
+

+ {mode === 'signin' + ? 'Sign in to your account' + : 'Create your account'} +

+
+ +
+
+ + + +
+ +
+ setEmail(e.target.value)} + required + maxLength={50} + className="appearance-none rounded-full relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-orange-500 focus:border-orange-500 focus:z-10 sm:text-sm" + placeholder="Enter your email" + /> +
+
+ +
+ +
+ setPassword(e.target.value)} + required + minLength={8} + maxLength={100} + className="appearance-none rounded-full relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-orange-500 focus:border-orange-500 focus:z-10 sm:text-sm" + placeholder="Enter your password" + /> +
+
+ + {error && ( +
{error}
+ )} + +
+ +
+
+ +
+
+
+
+
+
+ + {mode === 'signin' + ? 'New to our platform?' + : 'Already have an account?'} + +
+
+ +
+ + {mode === 'signin' + ? 'Create an account' + : 'Sign in to existing account'} + +
+
+
+
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/terminal.tsx b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/terminal.tsx new file mode 100644 index 000000000..a80112317 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/terminal.tsx @@ -0,0 +1,68 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Copy, Check } from 'lucide-react'; + +export function Terminal() { + const [terminalStep, setTerminalStep] = useState(0); + const [copied, setCopied] = useState(false); + const terminalSteps = [ + 'git clone https://github.com/nextjs/saas-starter', + 'pnpm install', + 'pnpm db:setup', + 'pnpm db:migrate', + 'pnpm db:seed', + 'pnpm dev 🎉', + ]; + + useEffect(() => { + const timer = setTimeout(() => { + setTerminalStep((prev) => + prev < terminalSteps.length - 1 ? prev + 1 : prev + ); + }, 500); + + return () => clearTimeout(timer); + }, [terminalStep]); + + const copyToClipboard = () => { + navigator.clipboard.writeText(terminalSteps.join('\n')); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+
+
+
+
+
+
+
+ +
+
+ {terminalSteps.map((step, index) => ( +
terminalStep ? 'opacity-0' : 'opacity-100'} transition-opacity duration-300`} + > + $ {step} +
+ ))} +
+
+
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/ui/avatar.tsx b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/ui/avatar.tsx new file mode 100644 index 000000000..852cb27a6 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/ui/avatar.tsx @@ -0,0 +1,53 @@ +"use client"; + +import * as React from "react"; +import { Avatar as AvatarPrimitive } from "radix-ui";; + +import { cn } from "@/lib/utils"; + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/ui/button.tsx b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/ui/button.tsx new file mode 100644 index 000000000..82cad4b47 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/ui/button.tsx @@ -0,0 +1,59 @@ +import * as React from "react"; +import { Slot as SlotPrimitive } from "radix-ui";; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline" + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9" + } + }, + defaultVariants: { + variant: "default", + size: "default" + } + } +); + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean; + }) { + const Comp = asChild ? SlotPrimitive.Slot : "button"; + + return ( + + ); +} + +export { Button, buttonVariants }; diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/ui/card.tsx b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/ui/card.tsx new file mode 100644 index 000000000..4733d5844 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent +}; diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/ui/dropdown-menu.tsx b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/ui/dropdown-menu.tsx new file mode 100644 index 000000000..f84f00586 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/ui/dropdown-menu.tsx @@ -0,0 +1,257 @@ +"use client"; + +import * as React from "react"; +import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";; +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean; + variant?: "default" | "destructive"; +}) { + return ( + + ); +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + ); +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ); +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + {children} + + + ); +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent +}; diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/ui/input.tsx b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/ui/input.tsx new file mode 100644 index 000000000..0316cc455 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ); +} + +export { Input }; diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/ui/label.tsx b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/ui/label.tsx new file mode 100644 index 000000000..387e860dd --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/ui/label.tsx @@ -0,0 +1,24 @@ +"use client"; + +import * as React from "react"; +import { Label as LabelPrimitive } from "radix-ui";; + +import { cn } from "@/lib/utils"; + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Label }; diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/ui/radio-group.tsx b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/ui/radio-group.tsx new file mode 100644 index 000000000..cdf5e9dd5 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/components/ui/radio-group.tsx @@ -0,0 +1,45 @@ +"use client"; + +import * as React from "react"; +import { RadioGroup as RadioGroupPrimitive } from "radix-ui";; +import { CircleIcon } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +function RadioGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function RadioGroupItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ); +} + +export { RadioGroup, RadioGroupItem }; diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/docker-compose.yml b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/docker-compose.yml new file mode 100644 index 000000000..278e2732f --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/docker-compose.yml @@ -0,0 +1,16 @@ + +services: + postgres: + image: postgres:16.4-alpine + container_name: next15_pages_router_saas_postgres + environment: + POSTGRES_DB: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "54322:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + +volumes: + postgres_data: diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/drizzle.config.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/drizzle.config.ts new file mode 100644 index 000000000..6f6cc8873 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/drizzle.config.ts @@ -0,0 +1,10 @@ +import type { Config } from 'drizzle-kit'; + +export default { + schema: './lib/db/schema.ts', + out: './lib/db/migrations', + dialect: 'postgresql', + dbCredentials: { + url: process.env.POSTGRES_URL!, + }, +} satisfies Config; diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/actions/account.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/actions/account.ts new file mode 100644 index 000000000..64add2623 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/actions/account.ts @@ -0,0 +1,168 @@ +import { z } from 'zod'; +import { eq, sql, and } from 'drizzle-orm'; +import { db } from '@/lib/db/drizzle'; +import { + users, + teamMembers, + activityLogs, + type NewActivityLog, + ActivityType +} from '@/lib/db/schema'; +import { comparePasswords, hashPassword } from '@/lib/auth/session'; +import { getUserWithTeam } from '@/lib/db/queries'; +import { + validatedActionWithUser +} from '@/lib/auth/middleware'; + +async function logActivity( + teamId: number | null | undefined, + userId: number, + type: ActivityType, + ipAddress?: string +) { + if (teamId === null || teamId === undefined) { + return; + } + const newActivity: NewActivityLog = { + teamId, + userId, + action: type, + ipAddress: ipAddress || '' + }; + await db.insert(activityLogs).values(newActivity); +} + +const updateAccountSchema = z.object({ + name: z.string().min(1, 'Name is required').max(100), + email: z.string().email('Invalid email address') +}); + +export const updateAccount = validatedActionWithUser( + updateAccountSchema, + async (data, _, user) => { + const { name, email } = data; + const userWithTeam = await getUserWithTeam(user.id); + + await Promise.all([ + db.update(users).set({ name, email }).where(eq(users.id, user.id)), + logActivity(userWithTeam?.teamId, user.id, ActivityType.UPDATE_ACCOUNT) + ]); + + return { name, success: 'Account updated successfully.' }; + } +); + +const updatePasswordSchema = z.object({ + currentPassword: z.string().min(8).max(100), + newPassword: z.string().min(8).max(100), + confirmPassword: z.string().min(8).max(100) +}); + +export const updatePassword = validatedActionWithUser( + updatePasswordSchema, + async (data, _, user) => { + const { currentPassword, newPassword, confirmPassword } = data; + + const isPasswordValid = await comparePasswords( + currentPassword, + user.passwordHash + ); + + if (!isPasswordValid) { + return { + currentPassword, + newPassword, + confirmPassword, + error: 'Current password is incorrect.' + }; + } + + if (currentPassword === newPassword) { + return { + currentPassword, + newPassword, + confirmPassword, + error: 'New password must be different from the current password.' + }; + } + + if (confirmPassword !== newPassword) { + return { + currentPassword, + newPassword, + confirmPassword, + error: 'New password and confirmation password do not match.' + }; + } + + const newPasswordHash = await hashPassword(newPassword); + const userWithTeam = await getUserWithTeam(user.id); + + await Promise.all([ + db + .update(users) + .set({ passwordHash: newPasswordHash }) + .where(eq(users.id, user.id)), + logActivity(userWithTeam?.teamId, user.id, ActivityType.UPDATE_PASSWORD) + ]); + + return { + success: 'Password updated successfully.' + }; + } +); + +const deleteAccountSchema = z.object({ + password: z.string().min(8).max(100) +}); + +export const deleteAccount = validatedActionWithUser( + deleteAccountSchema, + async (data, _, user) => { + const { password } = data; + + const isPasswordValid = await comparePasswords(password, user.passwordHash); + if (!isPasswordValid) { + return { + password, + error: 'Incorrect password. Account deletion failed.' + }; + } + + const userWithTeam = await getUserWithTeam(user.id); + + await logActivity( + userWithTeam?.teamId, + user.id, + ActivityType.DELETE_ACCOUNT + ); + + // Soft delete + await db + .update(users) + .set({ + deletedAt: sql`CURRENT_TIMESTAMP`, + email: sql`CONCAT(email, '-', id, '-deleted')` + }) + .where(eq(users.id, user.id)); + + if (userWithTeam?.teamId) { + await db + .delete(teamMembers) + .where( + and( + eq(teamMembers.userId, user.id), + eq(teamMembers.teamId, userWithTeam.teamId) + ) + ); + } + + // Note: In Pages Router, cookie deletion and redirect must be handled client-side + // Return success and let the component handle redirect + return { + success: 'Account deleted successfully.', + redirectTo: '/sign-in', + clearSession: true + }; + } +); diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/actions/auth.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/actions/auth.ts new file mode 100644 index 000000000..df8062680 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/actions/auth.ts @@ -0,0 +1,9 @@ +// Client-side signout helper for Pages Router +// The actual session deletion happens via API route + +export async function signOut() { + // In Pages Router, we handle signout client-side by calling an API route + // or using router.push after cookie deletion + // This is a placeholder that gets called from the header component + return Promise.resolve(); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/actions/team.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/actions/team.ts new file mode 100644 index 000000000..af87fff0f --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/actions/team.ts @@ -0,0 +1,129 @@ +import { z } from 'zod'; +import { and, eq } from 'drizzle-orm'; +import { db } from '@/lib/db/drizzle'; +import { + users, + teamMembers, + activityLogs, + invitations, + type NewActivityLog, + ActivityType +} from '@/lib/db/schema'; +import { getUser, getUserWithTeam } from '@/lib/db/queries'; +import { + validatedAction, + validatedActionWithUser +} from '@/lib/auth/middleware'; + +async function logActivity( + teamId: number | null | undefined, + userId: number, + type: ActivityType, + ipAddress?: string +) { + if (teamId === null || teamId === undefined) { + return; + } + const newActivity: NewActivityLog = { + teamId, + userId, + action: type, + ipAddress: ipAddress || '' + }; + await db.insert(activityLogs).values(newActivity); +} + +const removeTeamMemberSchema = z.object({ + memberId: z.number() +}); + +export const removeTeamMember = validatedActionWithUser( + removeTeamMemberSchema, + async (data, _, user) => { + const { memberId } = data; + const userWithTeam = await getUserWithTeam(user.id); + + if (!userWithTeam?.teamId) { + return { error: 'User is not part of a team' }; + } + + await db + .delete(teamMembers) + .where( + and( + eq(teamMembers.id, memberId), + eq(teamMembers.teamId, userWithTeam.teamId) + ) + ); + + await logActivity( + userWithTeam.teamId, + user.id, + ActivityType.REMOVE_TEAM_MEMBER + ); + + return { success: 'Team member removed successfully' }; + } +); + +const inviteTeamMemberSchema = z.object({ + email: z.string().email('Invalid email address'), + role: z.enum(['member', 'owner']) +}); + +export const inviteTeamMember = validatedActionWithUser( + inviteTeamMemberSchema, + async (data, _, user) => { + const { email, role } = data; + const userWithTeam = await getUserWithTeam(user.id); + + if (!userWithTeam?.teamId) { + return { error: 'User is not part of a team' }; + } + + const existingMember = await db + .select() + .from(users) + .leftJoin(teamMembers, eq(users.id, teamMembers.userId)) + .where( + and(eq(users.email, email), eq(teamMembers.teamId, userWithTeam.teamId)) + ) + .limit(1); + + if (existingMember.length > 0) { + return { error: 'User is already a member of this team' }; + } + + const existingInvitation = await db + .select() + .from(invitations) + .where( + and( + eq(invitations.email, email), + eq(invitations.teamId, userWithTeam.teamId), + eq(invitations.status, 'pending') + ) + ) + .limit(1); + + if (existingInvitation.length > 0) { + return { error: 'An invitation has already been sent to this email' }; + } + + await db.insert(invitations).values({ + teamId: userWithTeam.teamId, + email, + role, + invitedBy: user.id, + status: 'pending' + }); + + await logActivity( + userWithTeam.teamId, + user.id, + ActivityType.INVITE_TEAM_MEMBER + ); + + return { success: 'Invitation sent successfully' }; + } +); diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/auth/middleware.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/auth/middleware.ts new file mode 100644 index 000000000..a17f25e0d --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/auth/middleware.ts @@ -0,0 +1,75 @@ +import { z } from 'zod'; +import { TeamDataWithMembers, User } from '@/lib/db/schema'; +import { getTeamForUser, getUser } from '@/lib/db/queries'; +import { redirect } from 'next/navigation'; + +export type ActionState = { + error?: string; + success?: string; + [key: string]: any; // This allows for additional properties +}; + +type ValidatedActionFunction, T> = ( + data: z.infer, + formData: FormData +) => Promise; + +export function validatedAction, T>( + schema: S, + action: ValidatedActionFunction +) { + return async (prevState: ActionState, formData: FormData) => { + const result = schema.safeParse(Object.fromEntries(formData)); + if (!result.success) { + return { error: result.error.errors[0].message }; + } + + return action(result.data, formData); + }; +} + +type ValidatedActionWithUserFunction, T> = ( + data: z.infer, + formData: FormData, + user: User +) => Promise; + +export function validatedActionWithUser, T>( + schema: S, + action: ValidatedActionWithUserFunction +) { + return async (prevState: ActionState, formData: FormData) => { + const user = await getUser(); + if (!user) { + throw new Error('User is not authenticated'); + } + + const result = schema.safeParse(Object.fromEntries(formData)); + if (!result.success) { + return { error: result.error.errors[0].message }; + } + + return action(result.data, formData, user); + }; +} + +type ActionWithTeamFunction = ( + formData: FormData, + team: TeamDataWithMembers +) => Promise; + +export function withTeam(action: ActionWithTeamFunction) { + return async (formData: FormData): Promise => { + const user = await getUser(); + if (!user) { + redirect('/sign-in'); + } + + const team = await getTeamForUser(); + if (!team) { + throw new Error('Team not found'); + } + + return action(formData, team); + }; +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/auth/session.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/auth/session.ts new file mode 100644 index 000000000..11350e1e7 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/auth/session.ts @@ -0,0 +1,61 @@ +import { compare, hash } from 'bcryptjs'; +import { SignJWT, jwtVerify } from 'jose'; +import type { NextApiResponse } from 'next'; +import { NewUser } from '@/lib/db/schema'; + +const key = new TextEncoder().encode(process.env.AUTH_SECRET); +const SALT_ROUNDS = 10; + +export async function hashPassword(password: string) { + return hash(password, SALT_ROUNDS); +} + +export async function comparePasswords( + plainTextPassword: string, + hashedPassword: string +) { + return compare(plainTextPassword, hashedPassword); +} + +export type SessionData = { + user: { id: number }; + expires: string; +}; + +export async function signToken(payload: SessionData) { + return await new SignJWT(payload) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('1 day from now') + .sign(key); +} + +export async function verifyToken(input: string) { + const { payload } = await jwtVerify(input, key, { + algorithms: ['HS256'], + }); + return payload as SessionData; +} + +// Pages Router compatible - accepts cookie value directly +export async function getSession(sessionCookieValue?: string) { + if (!sessionCookieValue) return null; + return await verifyToken(sessionCookieValue); +} + +// Pages Router compatible - sets cookie via NextApiResponse +export async function setSession(user: NewUser, res?: NextApiResponse) { + const expiresInOneDay = new Date(Date.now() + 24 * 60 * 60 * 1000); + const session: SessionData = { + user: { id: user.id! }, + expires: expiresInOneDay.toISOString(), + }; + const encryptedSession = await signToken(session); + + if (res) { + // For API routes + res.setHeader('Set-Cookie', `session=${encryptedSession}; Path=/; HttpOnly; Secure; SameSite=Lax; Expires=${expiresInOneDay.toUTCString()}`); + } + + return encryptedSession; +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/drizzle.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/drizzle.ts new file mode 100644 index 000000000..7dc4a1b4f --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/drizzle.ts @@ -0,0 +1,13 @@ +import { drizzle } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; +import * as schema from './schema'; +import dotenv from 'dotenv'; + +dotenv.config(); + +if (!process.env.POSTGRES_URL) { + throw new Error('POSTGRES_URL environment variable is not set'); +} + +export const client = postgres(process.env.POSTGRES_URL); +export const db = drizzle(client, { schema }); diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/migrations/0000_soft_the_anarchist.sql b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/migrations/0000_soft_the_anarchist.sql new file mode 100644 index 000000000..b3dae0509 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/migrations/0000_soft_the_anarchist.sql @@ -0,0 +1,88 @@ +CREATE TABLE IF NOT EXISTS "activity_logs" ( + "id" serial PRIMARY KEY NOT NULL, + "team_id" integer NOT NULL, + "user_id" integer, + "action" text NOT NULL, + "timestamp" timestamp DEFAULT now() NOT NULL, + "ip_address" varchar(45) +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "invitations" ( + "id" serial PRIMARY KEY NOT NULL, + "team_id" integer NOT NULL, + "email" varchar(255) NOT NULL, + "role" varchar(50) NOT NULL, + "invited_by" integer NOT NULL, + "invited_at" timestamp DEFAULT now() NOT NULL, + "status" varchar(20) DEFAULT 'pending' NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "team_members" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" integer NOT NULL, + "team_id" integer NOT NULL, + "role" varchar(50) NOT NULL, + "joined_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "teams" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar(100) NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + "stripe_customer_id" text, + "stripe_subscription_id" text, + "stripe_product_id" text, + "plan_name" varchar(50), + "subscription_status" varchar(20), + CONSTRAINT "teams_stripe_customer_id_unique" UNIQUE("stripe_customer_id"), + CONSTRAINT "teams_stripe_subscription_id_unique" UNIQUE("stripe_subscription_id") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "users" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar(100), + "email" varchar(255) NOT NULL, + "password_hash" text NOT NULL, + "role" varchar(20) DEFAULT 'member' NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + "deleted_at" timestamp, + CONSTRAINT "users_email_unique" UNIQUE("email") +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "activity_logs" ADD CONSTRAINT "activity_logs_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "activity_logs" ADD CONSTRAINT "activity_logs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "invitations" ADD CONSTRAINT "invitations_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "invitations" ADD CONSTRAINT "invitations_invited_by_users_id_fk" FOREIGN KEY ("invited_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "team_members" ADD CONSTRAINT "team_members_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "team_members" ADD CONSTRAINT "team_members_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/migrations/meta/0000_snapshot.json b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/migrations/meta/0000_snapshot.json new file mode 100644 index 000000000..622eb9779 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/migrations/meta/0000_snapshot.json @@ -0,0 +1,389 @@ +{ + "id": "261fd993-fb2c-43e7-89d6-cd58786c5f58", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.activity_logs": { + "name": "activity_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_address": { + "name": "ip_address", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "activity_logs_team_id_teams_id_fk": { + "name": "activity_logs_team_id_teams_id_fk", + "tableFrom": "activity_logs", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_logs_user_id_users_id_fk": { + "name": "activity_logs_user_id_users_id_fk", + "tableFrom": "activity_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.invitations": { + "name": "invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "invited_at": { + "name": "invited_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + } + }, + "indexes": {}, + "foreignKeys": { + "invitations_team_id_teams_id_fk": { + "name": "invitations_team_id_teams_id_fk", + "tableFrom": "invitations", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "invitations_invited_by_users_id_fk": { + "name": "invitations_invited_by_users_id_fk", + "tableFrom": "invitations", + "tableTo": "users", + "columnsFrom": [ + "invited_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.team_members": { + "name": "team_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_members_user_id_users_id_fk": { + "name": "team_members_user_id_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "team_members_team_id_teams_id_fk": { + "name": "team_members_team_id_teams_id_fk", + "tableFrom": "team_members", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_product_id": { + "name": "stripe_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_name": { + "name": "plan_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "subscription_status": { + "name": "subscription_status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "teams_stripe_customer_id_unique": { + "name": "teams_stripe_customer_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_customer_id" + ] + }, + "teams_stripe_subscription_id_unique": { + "name": "teams_stripe_subscription_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_subscription_id" + ] + } + } + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + } + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/migrations/meta/_journal.json b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/migrations/meta/_journal.json new file mode 100644 index 000000000..fd4447417 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/migrations/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1726443359662, + "tag": "0000_soft_the_anarchist", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/queries.server.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/queries.server.ts new file mode 100644 index 000000000..c3233773c --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/queries.server.ts @@ -0,0 +1,133 @@ +// Server-only queries for Pages Router +// These functions use next/headers and should only be imported in getServerSideProps or API routes +import 'server-only'; +import { desc, and, eq, isNull } from 'drizzle-orm'; +import { db } from './drizzle'; +import { activityLogs, teamMembers, teams, users } from './schema'; +import { cookies } from 'next/headers'; +import { verifyToken } from '@/lib/auth/session'; + +export async function getUser() { + const sessionCookie = (await cookies()).get('session'); + if (!sessionCookie || !sessionCookie.value) { + return null; + } + + const sessionData = await verifyToken(sessionCookie.value); + if ( + !sessionData || + !sessionData.user || + typeof sessionData.user.id !== 'number' + ) { + return null; + } + + if (new Date(sessionData.expires) < new Date()) { + return null; + } + + const user = await db + .select() + .from(users) + .where(and(eq(users.id, sessionData.user.id), isNull(users.deletedAt))) + .limit(1); + + if (user.length === 0) { + return null; + } + + return user[0]; +} + +export async function getTeamByStripeCustomerId(customerId: string) { + const result = await db + .select() + .from(teams) + .where(eq(teams.stripeCustomerId, customerId)) + .limit(1); + + return result.length > 0 ? result[0] : null; +} + +export async function updateTeamSubscription( + teamId: number, + subscriptionData: { + stripeSubscriptionId: string | null; + stripeProductId: string | null; + planName: string | null; + subscriptionStatus: string; + } +) { + await db + .update(teams) + .set({ + ...subscriptionData, + updatedAt: new Date() + }) + .where(eq(teams.id, teamId)); +} + +export async function getUserWithTeam(userId: number) { + const result = await db + .select({ + user: users, + teamId: teamMembers.teamId + }) + .from(users) + .leftJoin(teamMembers, eq(users.id, teamMembers.userId)) + .where(eq(users.id, userId)) + .limit(1); + + return result[0]; +} + +export async function getActivityLogs() { + const user = await getUser(); + if (!user) { + throw new Error('User not authenticated'); + } + + return await db + .select({ + id: activityLogs.id, + action: activityLogs.action, + timestamp: activityLogs.timestamp, + ipAddress: activityLogs.ipAddress, + userName: users.name + }) + .from(activityLogs) + .leftJoin(users, eq(activityLogs.userId, users.id)) + .where(eq(activityLogs.userId, user.id)) + .orderBy(desc(activityLogs.timestamp)) + .limit(10); +} + +export async function getTeamForUser() { + const user = await getUser(); + if (!user) { + return null; + } + + const result = await db.query.teamMembers.findFirst({ + where: eq(teamMembers.userId, user.id), + with: { + team: { + with: { + teamMembers: { + with: { + user: { + columns: { + id: true, + name: true, + email: true + } + } + } + } + } + } + } + }); + + return result?.team || null; +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/queries.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/queries.ts new file mode 100644 index 000000000..348de1250 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/queries.ts @@ -0,0 +1,129 @@ +import { desc, and, eq, isNull } from 'drizzle-orm'; +import { db } from './drizzle'; +import { activityLogs, teamMembers, teams, users } from './schema'; +import { verifyToken } from '@/lib/auth/session'; + +// Pages Router compatible version - accepts cookie value directly +export async function getUser(sessionCookieValue?: string) { + if (!sessionCookieValue) { + return null; + } + + const sessionData = await verifyToken(sessionCookieValue); + if ( + !sessionData || + !sessionData.user || + typeof sessionData.user.id !== 'number' + ) { + return null; + } + + if (new Date(sessionData.expires) < new Date()) { + return null; + } + + const user = await db + .select() + .from(users) + .where(and(eq(users.id, sessionData.user.id), isNull(users.deletedAt))) + .limit(1); + + if (user.length === 0) { + return null; + } + + return user[0]; +} + +export async function getTeamByStripeCustomerId(customerId: string) { + const result = await db + .select() + .from(teams) + .where(eq(teams.stripeCustomerId, customerId)) + .limit(1); + + return result.length > 0 ? result[0] : null; +} + +export async function updateTeamSubscription( + teamId: number, + subscriptionData: { + stripeSubscriptionId: string | null; + stripeProductId: string | null; + planName: string | null; + subscriptionStatus: string; + } +) { + await db + .update(teams) + .set({ + ...subscriptionData, + updatedAt: new Date() + }) + .where(eq(teams.id, teamId)); +} + +export async function getUserWithTeam(userId: number) { + const result = await db + .select({ + user: users, + teamId: teamMembers.teamId + }) + .from(users) + .leftJoin(teamMembers, eq(users.id, teamMembers.userId)) + .where(eq(users.id, userId)) + .limit(1); + + return result[0]; +} + +export async function getActivityLogs(sessionCookieValue?: string) { + const user = await getUser(sessionCookieValue); + if (!user) { + throw new Error('User not authenticated'); + } + + return await db + .select({ + id: activityLogs.id, + action: activityLogs.action, + timestamp: activityLogs.timestamp, + ipAddress: activityLogs.ipAddress, + userName: users.name + }) + .from(activityLogs) + .leftJoin(users, eq(activityLogs.userId, users.id)) + .where(eq(activityLogs.userId, user.id)) + .orderBy(desc(activityLogs.timestamp)) + .limit(10); +} + +export async function getTeamForUser(sessionCookieValue?: string) { + const user = await getUser(sessionCookieValue); + if (!user) { + return null; + } + + const result = await db.query.teamMembers.findFirst({ + where: eq(teamMembers.userId, user.id), + with: { + team: { + with: { + teamMembers: { + with: { + user: { + columns: { + id: true, + name: true, + email: true + } + } + } + } + } + } + } + }); + + return result?.team || null; +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/schema.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/schema.ts new file mode 100644 index 000000000..1d047ce66 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/schema.ts @@ -0,0 +1,142 @@ +import { + pgTable, + serial, + varchar, + text, + timestamp, + integer, +} from 'drizzle-orm/pg-core'; +import { relations } from 'drizzle-orm'; + +export const users = pgTable('users', { + id: serial('id').primaryKey(), + name: varchar('name', { length: 100 }), + email: varchar('email', { length: 255 }).notNull().unique(), + passwordHash: text('password_hash').notNull(), + role: varchar('role', { length: 20 }).notNull().default('member'), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + deletedAt: timestamp('deleted_at'), +}); + +export const teams = pgTable('teams', { + id: serial('id').primaryKey(), + name: varchar('name', { length: 100 }).notNull(), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + stripeCustomerId: text('stripe_customer_id').unique(), + stripeSubscriptionId: text('stripe_subscription_id').unique(), + stripeProductId: text('stripe_product_id'), + planName: varchar('plan_name', { length: 50 }), + subscriptionStatus: varchar('subscription_status', { length: 20 }), +}); + +export const teamMembers = pgTable('team_members', { + id: serial('id').primaryKey(), + userId: integer('user_id') + .notNull() + .references(() => users.id), + teamId: integer('team_id') + .notNull() + .references(() => teams.id), + role: varchar('role', { length: 50 }).notNull(), + joinedAt: timestamp('joined_at').notNull().defaultNow(), +}); + +export const activityLogs = pgTable('activity_logs', { + id: serial('id').primaryKey(), + teamId: integer('team_id') + .notNull() + .references(() => teams.id), + userId: integer('user_id').references(() => users.id), + action: text('action').notNull(), + timestamp: timestamp('timestamp').notNull().defaultNow(), + ipAddress: varchar('ip_address', { length: 45 }), +}); + +export const invitations = pgTable('invitations', { + id: serial('id').primaryKey(), + teamId: integer('team_id') + .notNull() + .references(() => teams.id), + email: varchar('email', { length: 255 }).notNull(), + role: varchar('role', { length: 50 }).notNull(), + invitedBy: integer('invited_by') + .notNull() + .references(() => users.id), + invitedAt: timestamp('invited_at').notNull().defaultNow(), + status: varchar('status', { length: 20 }).notNull().default('pending'), +}); + +export const teamsRelations = relations(teams, ({ many }) => ({ + teamMembers: many(teamMembers), + activityLogs: many(activityLogs), + invitations: many(invitations), +})); + +export const usersRelations = relations(users, ({ many }) => ({ + teamMembers: many(teamMembers), + invitationsSent: many(invitations), +})); + +export const invitationsRelations = relations(invitations, ({ one }) => ({ + team: one(teams, { + fields: [invitations.teamId], + references: [teams.id], + }), + invitedBy: one(users, { + fields: [invitations.invitedBy], + references: [users.id], + }), +})); + +export const teamMembersRelations = relations(teamMembers, ({ one }) => ({ + user: one(users, { + fields: [teamMembers.userId], + references: [users.id], + }), + team: one(teams, { + fields: [teamMembers.teamId], + references: [teams.id], + }), +})); + +export const activityLogsRelations = relations(activityLogs, ({ one }) => ({ + team: one(teams, { + fields: [activityLogs.teamId], + references: [teams.id], + }), + user: one(users, { + fields: [activityLogs.userId], + references: [users.id], + }), +})); + +export type User = typeof users.$inferSelect; +export type NewUser = typeof users.$inferInsert; +export type Team = typeof teams.$inferSelect; +export type NewTeam = typeof teams.$inferInsert; +export type TeamMember = typeof teamMembers.$inferSelect; +export type NewTeamMember = typeof teamMembers.$inferInsert; +export type ActivityLog = typeof activityLogs.$inferSelect; +export type NewActivityLog = typeof activityLogs.$inferInsert; +export type Invitation = typeof invitations.$inferSelect; +export type NewInvitation = typeof invitations.$inferInsert; +export type TeamDataWithMembers = Team & { + teamMembers: (TeamMember & { + user: Pick; + })[]; +}; + +export enum ActivityType { + SIGN_UP = 'SIGN_UP', + SIGN_IN = 'SIGN_IN', + SIGN_OUT = 'SIGN_OUT', + UPDATE_PASSWORD = 'UPDATE_PASSWORD', + DELETE_ACCOUNT = 'DELETE_ACCOUNT', + UPDATE_ACCOUNT = 'UPDATE_ACCOUNT', + CREATE_TEAM = 'CREATE_TEAM', + REMOVE_TEAM_MEMBER = 'REMOVE_TEAM_MEMBER', + INVITE_TEAM_MEMBER = 'INVITE_TEAM_MEMBER', + ACCEPT_INVITATION = 'ACCEPT_INVITATION', +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/seed.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/seed.ts new file mode 100644 index 000000000..66b60513e --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/seed.ts @@ -0,0 +1,118 @@ +import { stripe } from '../payments/stripe'; +import { db } from './drizzle'; +import { users, teams, teamMembers } from './schema'; +import { hashPassword } from '@/lib/auth/session'; +import { eq } from 'drizzle-orm'; + +async function createStripeProducts() { + console.log('Creating Stripe products and prices...'); + + const baseProduct = await stripe.products.create({ + name: 'Base', + description: 'Base subscription plan', + }); + + await stripe.prices.create({ + product: baseProduct.id, + unit_amount: 800, // $8 in cents + currency: 'usd', + recurring: { + interval: 'month', + trial_period_days: 7, + }, + }); + + const plusProduct = await stripe.products.create({ + name: 'Plus', + description: 'Plus subscription plan', + }); + + await stripe.prices.create({ + product: plusProduct.id, + unit_amount: 1200, // $12 in cents + currency: 'usd', + recurring: { + interval: 'month', + trial_period_days: 7, + }, + }); + + console.log('Stripe products and prices created successfully.'); +} + +async function seed() { + const email = 'test@test.com'; + const password = 'admin123'; + const passwordHash = await hashPassword(password); + + // Check if user already exists + const existingUsers = await db + .select() + .from(users) + .where(eq(users.email, email)) + .limit(1); + + let user; + if (existingUsers.length > 0) { + console.log('User already exists, skipping user creation.'); + user = existingUsers[0]; + } else { + const [newUser] = await db + .insert(users) + .values([ + { + email: email, + passwordHash: passwordHash, + role: "owner", + }, + ]) + .returning(); + user = newUser; + console.log('Initial user created.'); + } + + // Check if team already exists for this user + const existingTeamMembers = await db + .select() + .from(teamMembers) + .where(eq(teamMembers.userId, user.id)) + .limit(1); + + let team; + if (existingTeamMembers.length > 0) { + console.log('Team membership already exists, skipping team creation.'); + const existingTeams = await db + .select() + .from(teams) + .where(eq(teams.id, existingTeamMembers[0].teamId)) + .limit(1); + team = existingTeams[0]; + } else { + const [newTeam] = await db + .insert(teams) + .values({ + name: 'Test Team', + }) + .returning(); + team = newTeam; + + await db.insert(teamMembers).values({ + teamId: team.id, + userId: user.id, + role: 'owner', + }); + console.log('Team and team membership created.'); + } + + await createStripeProducts(); +} + +seed() + .catch((error) => { + console.error('Seed process failed:', error); + process.exit(1); + }) + .finally(() => { + console.log('Seed process finished. Exiting...'); + process.exit(0); + }); diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/setup.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/setup.ts new file mode 100644 index 000000000..c23d7d4fa --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/db/setup.ts @@ -0,0 +1,216 @@ +import { exec } from 'node:child_process'; +import { promises as fs } from 'node:fs'; +import { promisify } from 'node:util'; +import readline from 'node:readline'; +import crypto from 'node:crypto'; +import path from 'node:path'; +import os from 'node:os'; + +const execAsync = promisify(exec); + +function question(query: string): Promise { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + return new Promise((resolve) => + rl.question(query, (ans) => { + rl.close(); + resolve(ans); + }) + ); +} + +async function checkStripeCLI() { + console.log( + 'Step 1: Checking if Stripe CLI is installed and authenticated...' + ); + try { + await execAsync('stripe --version'); + console.log('Stripe CLI is installed.'); + + // Check if Stripe CLI is authenticated + try { + await execAsync('stripe config --list'); + console.log('Stripe CLI is authenticated.'); + } catch (error) { + console.log( + 'Stripe CLI is not authenticated or the authentication has expired.' + ); + console.log('Please run: stripe login'); + const answer = await question( + 'Have you completed the authentication? (y/n): ' + ); + if (answer.toLowerCase() !== 'y') { + console.log( + 'Please authenticate with Stripe CLI and run this script again.' + ); + process.exit(1); + } + + // Verify authentication after user confirms login + try { + await execAsync('stripe config --list'); + console.log('Stripe CLI authentication confirmed.'); + } catch (error) { + console.error( + 'Failed to verify Stripe CLI authentication. Please try again.' + ); + process.exit(1); + } + } + } catch (error) { + console.error( + 'Stripe CLI is not installed. Please install it and try again.' + ); + console.log('To install Stripe CLI, follow these steps:'); + console.log('1. Visit: https://docs.stripe.com/stripe-cli'); + console.log( + '2. Download and install the Stripe CLI for your operating system' + ); + console.log('3. After installation, run: stripe login'); + console.log( + 'After installation and authentication, please run this setup script again.' + ); + process.exit(1); + } +} + +async function getPostgresURL(): Promise { + console.log('Step 2: Setting up Postgres'); + const dbChoice = await question( + 'Do you want to use a local Postgres instance with Docker (L) or a remote Postgres instance (R)? (L/R): ' + ); + + if (dbChoice.toLowerCase() === 'l') { + console.log('Setting up local Postgres instance with Docker...'); + await setupLocalPostgres(); + return 'postgres://postgres:postgres@localhost:54322/postgres'; + } else { + console.log( + 'You can find Postgres databases at: https://vercel.com/marketplace?category=databases' + ); + return await question('Enter your POSTGRES_URL: '); + } +} + +async function setupLocalPostgres() { + console.log('Checking if Docker is installed...'); + try { + await execAsync('docker --version'); + console.log('Docker is installed.'); + } catch (error) { + console.error( + 'Docker is not installed. Please install Docker and try again.' + ); + console.log( + 'To install Docker, visit: https://docs.docker.com/get-docker/' + ); + process.exit(1); + } + + console.log('Creating docker-compose.yml file...'); + const dockerComposeContent = ` +services: + postgres: + image: postgres:16.4-alpine + container_name: next_saas_starter_postgres + environment: + POSTGRES_DB: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "54322:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + +volumes: + postgres_data: +`; + + await fs.writeFile( + path.join(process.cwd(), 'docker-compose.yml'), + dockerComposeContent + ); + console.log('docker-compose.yml file created.'); + + console.log('Starting Docker container with `docker compose up -d`...'); + try { + await execAsync('docker compose up -d'); + console.log('Docker container started successfully.'); + } catch (error) { + console.error( + 'Failed to start Docker container. Please check your Docker installation and try again.' + ); + process.exit(1); + } +} + +async function getStripeSecretKey(): Promise { + console.log('Step 3: Getting Stripe Secret Key'); + console.log( + 'You can find your Stripe Secret Key at: https://dashboard.stripe.com/test/apikeys' + ); + return await question('Enter your Stripe Secret Key: '); +} + +async function createStripeWebhook(): Promise { + console.log('Step 4: Creating Stripe webhook...'); + try { + const { stdout } = await execAsync('stripe listen --print-secret'); + const match = stdout.match(/whsec_[a-zA-Z0-9]+/); + if (!match) { + throw new Error('Failed to extract Stripe webhook secret'); + } + console.log('Stripe webhook created.'); + return match[0]; + } catch (error) { + console.error( + 'Failed to create Stripe webhook. Check your Stripe CLI installation and permissions.' + ); + if (os.platform() === 'win32') { + console.log( + 'Note: On Windows, you may need to run this script as an administrator.' + ); + } + throw error; + } +} + +function generateAuthSecret(): string { + console.log('Step 5: Generating AUTH_SECRET...'); + return crypto.randomBytes(32).toString('hex'); +} + +async function writeEnvFile(envVars: Record) { + console.log('Step 6: Writing environment variables to .env'); + const envContent = Object.entries(envVars) + .map(([key, value]) => `${key}=${value}`) + .join('\n'); + + await fs.writeFile(path.join(process.cwd(), '.env'), envContent); + console.log('.env file created with the necessary variables.'); +} + +async function main() { + await checkStripeCLI(); + + const POSTGRES_URL = await getPostgresURL(); + const STRIPE_SECRET_KEY = await getStripeSecretKey(); + const STRIPE_WEBHOOK_SECRET = await createStripeWebhook(); + const BASE_URL = 'http://localhost:3000'; + const AUTH_SECRET = generateAuthSecret(); + + await writeEnvFile({ + POSTGRES_URL, + STRIPE_SECRET_KEY, + STRIPE_WEBHOOK_SECRET, + BASE_URL, + AUTH_SECRET, + }); + + console.log('🎉 Setup completed successfully!'); +} + +main().catch(console.error); diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/payments/STRIPE_STUB_README.md b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/payments/STRIPE_STUB_README.md new file mode 100644 index 000000000..80111f5ca --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/payments/STRIPE_STUB_README.md @@ -0,0 +1,212 @@ +# Stripe Stub Mode + +This directory contains a stub implementation of the Stripe API that allows you to develop and test the SaaS application without real Stripe API keys. + +## Why Use Stub Mode? + +- **No Stripe Account Required**: Develop without signing up for Stripe +- **Faster Development**: No network latency from API calls +- **Offline Development**: Work without internet connection +- **Predictable Testing**: Consistent mock data for testing +- **No API Limits**: No rate limiting or quotas +- **Free**: No charges or test mode transactions + +## How to Enable Stub Mode + +### Option 1: Environment Variable (Recommended) + +Add to your [.env](.env) file: + +```bash +STRIPE_MODE=stub +``` + +### Option 2: Invalid/Missing API Keys + +The stub mode automatically activates if: +- `STRIPE_SECRET_KEY` is not set +- `STRIPE_SECRET_KEY` is set to `sk_test_***` (example value) + +## What Gets Mocked + +### Products & Prices + +The stub provides three pre-configured products: + +1. **Basic Plan** + - Product ID: `prod_stub_basic` + - Price ID: `price_stub_basic_monthly` + - Price: $10.00/month + - Trial: 14 days + +2. **Pro Plan** + - Product ID: `prod_stub_pro` + - Price ID: `price_stub_pro_monthly` + - Price: $25.00/month + - Trial: 14 days + +3. **Enterprise Plan** + - Product ID: `prod_stub_enterprise` + - Price ID: `price_stub_enterprise_monthly` + - Price: $100.00/month + - Trial: 14 days + +### Checkout Sessions + +When you create a checkout session in stub mode: +- A mock session ID is generated (`cs_stub_*`) +- A mock customer ID is created (`cus_stub_*`) +- A mock subscription ID is created (`sub_stub_*`) +- The user is redirected to the success URL +- All subscription data is stored in your local database + +### Customer Portal + +The billing portal session creates: +- A mock portal session ID (`bps_stub_*`) +- A redirect to your dashboard with a query param +- All configuration options are accepted + +### Webhooks + +Webhook events can be simulated by: +1. Calling the webhook endpoint directly +2. Sending mock event data in the request body +3. The stub will parse and process the event + +Example webhook event: +```json +{ + "type": "customer.subscription.updated", + "data": { + "object": { + "id": "sub_stub_123", + "customer": "cus_stub_123", + "status": "active", + "items": { + "data": [{ + "plan": { + "product": { + "id": "prod_stub_basic", + "name": "Basic Plan" + } + } + }] + } + } + } +} +``` + +## Testing the Checkout Flow + +1. **Navigate to Pricing Page**: `/pricing` +2. **Click on a Plan**: Choose any plan +3. **Get Redirected**: You'll see a "checkout" URL with session ID +4. **Auto-Complete**: The checkout automatically succeeds +5. **Return to Dashboard**: You're logged in with active subscription + +The entire flow works without leaving your application! + +## Console Logging + +When stub mode is active, you'll see: +``` +🎭 Running in Stripe STUB mode - no real API calls will be made +``` + +All stub operations log to console with `[Stripe Stub]` prefix: +``` +[Stripe Stub] Creating checkout session: {...} +[Stripe Stub] Retrieving subscription: sub_stub_123 +[Stripe Stub] Listing products: {...} +``` + +## Subscription Statuses + +The stub supports all subscription statuses: +- `trialing` (default for new subscriptions) +- `active` +- `canceled` +- `unpaid` +- `past_due` + +## Limitations + +The stub implementation: +- ✅ Supports all core SaaS functionality +- ✅ Works with the pricing page +- ✅ Processes checkout sessions +- ✅ Handles webhooks +- ✅ Manages customer portal +- ❌ Does NOT process real payments +- ❌ Does NOT send Stripe emails +- ❌ Does NOT create actual Stripe resources +- ❌ Does NOT validate real payment methods + +## Switching to Real Stripe + +When you're ready to use real Stripe: + +1. Sign up at [stripe.com](https://stripe.com) +2. Get your API keys from the Stripe Dashboard +3. Update your `.env` file: + ```bash + STRIPE_SECRET_KEY=sk_test_your_real_key + STRIPE_WEBHOOK_SECRET=whsec_your_real_secret + # Remove or comment out: + # STRIPE_MODE=stub + ``` +4. Restart your development server +5. Set up real products and prices in Stripe Dashboard +6. Configure webhook endpoints in Stripe + +## Files + +- [stripe-stub.ts](stripe-stub.ts) - Stub implementation +- [stripe.ts](stripe.ts) - Main Stripe client (auto-detects stub mode) +- `STRIPE_STUB_README.md` - This file + +## Development Tips + +1. **Use stub mode for UI development** - Build out forms, buttons, and flows +2. **Use real Stripe for payment testing** - Verify actual payment processing +3. **Keep stub data simple** - Extend the stub as needed for your use case +4. **Log everything** - The stub logs all operations for debugging + +## Extending the Stub + +To add more mock products or customize behavior, edit [stripe-stub.ts](stripe-stub.ts): + +```typescript +// Add a new product +{ + id: 'prod_stub_custom', + name: 'Custom Plan', + description: 'A custom plan', + active: true, +} +``` + +## Troubleshooting + +**Q: My checkout isn't working** +A: Check the console for `[Stripe Stub]` logs to see what's happening + +**Q: I'm not seeing stub mode** +A: Verify `STRIPE_MODE=stub` in your `.env` and restart the dev server + +**Q: Can I use this in production?** +A: No! Stub mode is for development only. Always use real Stripe in production. + +**Q: How do I test webhooks?** +A: Send POST requests to `/api/stripe/webhook` with mock event data + +## Support + +For issues with the stub implementation, check: +1. Console logs for `[Stripe Stub]` messages +2. Your `.env` configuration +3. The stub source code in [stripe-stub.ts](stripe-stub.ts) + +For real Stripe issues, see [Stripe Documentation](https://stripe.com/docs). diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/payments/TESTING_GUIDE.md b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/payments/TESTING_GUIDE.md new file mode 100644 index 000000000..c991dde04 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/payments/TESTING_GUIDE.md @@ -0,0 +1,223 @@ +# Testing Stripe Stub Mode + +This guide shows you how to test the application using Stripe stub mode. + +## Quick Start + +1. **Enable stub mode** (already done in your `.env`): + ```bash + STRIPE_MODE=stub + ``` + +2. **Start your development server**: + ```bash + pnpm dev + ``` + +3. **Look for the stub mode indicator** in your console: + ``` + 🎭 Running in Stripe STUB mode - no real API calls will be made + ``` + +## Testing Scenarios + +### Scenario 1: View Pricing + +1. Navigate to `http://localhost:3000/pricing` +2. You should see pricing plans displayed +3. The plans come from the stub (not real Stripe) + +**Expected Stub Data:** +- Basic Plan: $10/month +- Pro Plan: $25/month +- Enterprise Plan: $100/month + +### Scenario 2: Checkout Flow + +1. Go to `/pricing` +2. Click "Subscribe" on any plan +3. You'll be redirected to checkout +4. The stub automatically completes checkout +5. You land on `/dashboard` with an active subscription + +**What happens behind the scenes:** +``` +[Stripe Stub] Creating checkout session +[Stripe Stub] Retrieving checkout session +[Stripe Stub] Retrieving subscription +``` + +**Database changes:** +- Team gets `stripeCustomerId` (e.g., `cus_stub_123456`) +- Team gets `stripeSubscriptionId` (e.g., `sub_stub_123456`) +- Team gets `stripeProductId` (e.g., `prod_stub_basic`) +- Team `subscriptionStatus` set to `trialing` + +### Scenario 3: Customer Portal + +1. From your dashboard, click "Manage Subscription" +2. You'll be redirected to a stub portal URL +3. The URL contains `?stub=billing-portal` query param + +**Expected behavior:** +- No actual Stripe portal is opened +- You stay on your local application +- Can simulate subscription changes via webhooks + +### Scenario 4: Webhook Simulation + +Use curl or Postman to simulate a webhook: + +```bash +curl -X POST http://localhost:3000/api/stripe/webhook \ + -H "Content-Type: application/json" \ + -H "stripe-signature: stub_signature" \ + -d '{ + "type": "customer.subscription.updated", + "data": { + "object": { + "id": "sub_stub_123", + "customer": "cus_stub_123", + "status": "active", + "items": { + "data": [{ + "plan": { + "product": { + "id": "prod_stub_basic", + "name": "Basic Plan" + } + } + }] + } + } + } + }' +``` + +**Expected response:** +```json +{ "received": true } +``` + +### Scenario 5: Subscription Cancellation + +Simulate a cancellation webhook: + +```bash +curl -X POST http://localhost:3000/api/stripe/webhook \ + -H "Content-Type: application/json" \ + -H "stripe-signature: stub_signature" \ + -d '{ + "type": "customer.subscription.deleted", + "data": { + "object": { + "id": "sub_stub_123", + "customer": "cus_stub_123", + "status": "canceled" + } + } + }' +``` + +**Expected database changes:** +- `stripeSubscriptionId` → `null` +- `stripeProductId` → `null` +- `planName` → `null` +- `subscriptionStatus` → `canceled` + +## Debugging Tips + +### Enable Verbose Logging + +All stub operations log to console. Look for: +``` +[Stripe Stub] Creating checkout session: { priceId: 'price_stub_basic_monthly', ... } +[Stripe Stub] Retrieving subscription: sub_stub_123456 +``` + +### Check Database State + +After any operation, check your database: + +```sql +SELECT + id, + name, + stripeCustomerId, + stripeSubscriptionId, + stripeProductId, + planName, + subscriptionStatus +FROM teams; +``` + +### Common Issues + +**Issue: Not seeing stub mode message** +- ✅ Solution: Make sure `.env` has `STRIPE_MODE=stub` +- ✅ Solution: Restart your dev server + +**Issue: Checkout redirects to real Stripe** +- ✅ Solution: Check console for stub mode indicator +- ✅ Solution: Verify `STRIPE_MODE=stub` is set correctly + +**Issue: Webhook fails** +- ✅ Solution: Include `stripe-signature` header (any value works) +- ✅ Solution: Check request body matches expected format + +## Comparing Stub vs Real Stripe + +| Feature | Stub Mode | Real Stripe | +|---------|-----------|-------------| +| Speed | Instant ⚡ | Network latency | +| Cost | Free 💰 | Test mode free, production paid | +| Internet Required | No 📡 | Yes | +| Payment Processing | Simulated ✨ | Real | +| Webhook Events | Manual 🔧 | Automatic | +| Customer Portal | Mocked 🎭 | Real Stripe UI | +| Product Management | Hardcoded 📝 | Dashboard managed | + +## When to Use Each Mode + +### Use Stub Mode For: +- ✅ Initial development +- ✅ UI/UX work +- ✅ Offline development +- ✅ Fast iteration +- ✅ Testing subscription logic +- ✅ CI/CD pipelines + +### Use Real Stripe For: +- ✅ Payment flow testing +- ✅ Webhook integration testing +- ✅ Customer portal testing +- ✅ Pre-production testing +- ✅ Production deployment + +## Switching Between Modes + +### Enable Stub Mode +```bash +# In .env +STRIPE_MODE=stub +``` + +### Enable Real Stripe +```bash +# In .env +# STRIPE_MODE=stub # Comment out or remove this line +``` + +Then restart your dev server. + +## Next Steps + +1. **Build your features** using stub mode +2. **Test thoroughly** with real Stripe in test mode +3. **Deploy to production** with real Stripe in live mode + +## API Reference + +See [stripe-stub.ts](stripe-stub.ts) for complete stub implementation details. + +For real Stripe API, see [Stripe API Docs](https://stripe.com/docs/api). diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/payments/stripe-stub.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/payments/stripe-stub.ts new file mode 100644 index 000000000..270a91322 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/payments/stripe-stub.ts @@ -0,0 +1,354 @@ +/** + * Stripe Stub Implementation + * + * This file provides mock implementations of Stripe API calls for development + * without requiring real Stripe API keys. + * + * To use: Set STRIPE_MODE=stub in your .env file + */ + +export interface StubCheckoutSession { + id: string; + url: string; + customer: { id: string }; + subscription: string; + client_reference_id: string; + payment_method_types: string[]; + line_items: Array<{ price: string; quantity: number }>; + mode: string; + success_url: string; + cancel_url: string; + allow_promotion_codes: boolean; + subscription_data?: { trial_period_days: number }; +} + +export interface StubBillingPortalSession { + id: string; + url: string; + customer: string; + return_url: string; + configuration: string; +} + +export interface StubSubscription { + id: string; + customer: string; + status: 'active' | 'trialing' | 'canceled' | 'unpaid' | 'past_due'; + items: { + data: Array<{ + id: string; + price: StubPrice; + plan?: StubPrice; + }>; + }; +} + +export interface StubPrice { + id: string; + product: string | StubProduct; + unit_amount: number; + currency: string; + recurring?: { + interval: 'day' | 'week' | 'month' | 'year'; + trial_period_days?: number; + }; +} + +export interface StubProduct { + id: string; + name: string; + description: string | null; + active: boolean; + default_price?: string | StubPrice; +} + +export interface StubBillingPortalConfiguration { + id: string; + business_profile: { headline: string }; + features: Record; +} + +export interface StubEvent { + id: string; + type: string; + data: { + object: StubSubscription | Record; + }; +} + +/** + * Stub Stripe Client + * Mimics the Stripe SDK structure but returns mock data + */ +export class StripeStub { + checkout = { + sessions: { + create: async (params: Record): Promise => { + const sessionId = `cs_stub_${Date.now()}`; + const customerId = (params.customer as string) || `cus_stub_${Date.now()}`; + const subscriptionId = `sub_stub_${Date.now()}`; + + console.log('[Stripe Stub] Creating checkout session:', params); + + return { + id: sessionId, + url: `${params.success_url}`.replace('{CHECKOUT_SESSION_ID}', sessionId), + customer: { id: customerId }, + subscription: subscriptionId, + client_reference_id: params.client_reference_id as string, + payment_method_types: params.payment_method_types as string[], + line_items: params.line_items as Array<{ price: string; quantity: number }>, + mode: params.mode as string, + success_url: params.success_url as string, + cancel_url: params.cancel_url as string, + allow_promotion_codes: params.allow_promotion_codes as boolean, + subscription_data: params.subscription_data as { trial_period_days: number } | undefined, + }; + }, + retrieve: async ( + sessionId: string, + options?: { expand?: string[] } + ): Promise => { + console.log('[Stripe Stub] Retrieving checkout session:', sessionId); + + const customerId = `cus_stub_${Date.now()}`; + const subscriptionId = `sub_stub_${Date.now()}`; + + return { + id: sessionId, + url: `http://localhost:3000/dashboard`, + customer: { id: customerId }, + subscription: subscriptionId, + client_reference_id: '1', // Default user ID + payment_method_types: ['card'], + line_items: [{ price: 'price_stub_basic', quantity: 1 }], + mode: 'subscription', + success_url: 'http://localhost:3000/api/stripe/checkout', + cancel_url: 'http://localhost:3000/pricing', + allow_promotion_codes: true, + subscription_data: { trial_period_days: 14 }, + }; + }, + }, + }; + + subscriptions = { + retrieve: async ( + subscriptionId: string, + options?: { expand?: string[] } + ): Promise => { + console.log('[Stripe Stub] Retrieving subscription:', subscriptionId); + + return { + id: subscriptionId, + customer: `cus_stub_${Date.now()}`, + status: 'trialing', + items: { + data: [ + { + id: 'si_stub_1', + price: { + id: 'price_stub_basic', + product: { + id: 'prod_stub_basic', + name: 'Basic Plan', + description: 'Basic tier subscription', + active: true, + }, + unit_amount: 1000, + currency: 'usd', + recurring: { + interval: 'month', + trial_period_days: 14, + }, + }, + }, + ], + }, + }; + }, + }; + + billingPortal = { + configurations: { + list: async (): Promise<{ data: StubBillingPortalConfiguration[] }> => { + console.log('[Stripe Stub] Listing billing portal configurations'); + return { + data: [ + { + id: 'bpc_stub_1', + business_profile: { headline: 'Manage your subscription' }, + features: {}, + }, + ], + }; + }, + create: async (params: Record): Promise => { + console.log('[Stripe Stub] Creating billing portal configuration:', params); + return { + id: `bpc_stub_${Date.now()}`, + business_profile: params.business_profile as { headline: string }, + features: params.features as Record, + }; + }, + }, + sessions: { + create: async (params: { + customer: string; + return_url: string; + configuration?: string; + }): Promise => { + console.log('[Stripe Stub] Creating billing portal session:', params); + return { + id: `bps_stub_${Date.now()}`, + url: `http://localhost:3000/dashboard?stub=billing-portal`, + customer: params.customer, + return_url: params.return_url, + configuration: params.configuration || 'bpc_stub_default', + }; + }, + }, + }; + + products = { + list: async (params?: { + active?: boolean; + expand?: string[]; + }): Promise<{ data: StubProduct[] }> => { + console.log('[Stripe Stub] Listing products:', params); + return { + data: [ + { + id: 'prod_stub_basic', + name: 'Basic Plan', + description: 'Perfect for getting started', + active: true, + default_price: 'price_stub_basic_monthly', + }, + { + id: 'prod_stub_pro', + name: 'Pro Plan', + description: 'For growing teams', + active: true, + default_price: 'price_stub_pro_monthly', + }, + { + id: 'prod_stub_enterprise', + name: 'Enterprise Plan', + description: 'For large organizations', + active: true, + default_price: 'price_stub_enterprise_monthly', + }, + ], + }; + }, + retrieve: async (productId: string): Promise => { + console.log('[Stripe Stub] Retrieving product:', productId); + return { + id: productId, + name: 'Basic Plan', + description: 'Perfect for getting started', + active: true, + }; + }, + }; + + prices = { + list: async (params?: { + active?: boolean; + type?: string; + product?: string; + expand?: string[]; + }): Promise<{ data: StubPrice[] }> => { + console.log('[Stripe Stub] Listing prices:', params); + return { + data: [ + { + id: 'price_stub_basic_monthly', + product: { + id: 'prod_stub_basic', + name: 'Basic Plan', + description: 'Perfect for getting started', + active: true, + }, + unit_amount: 1000, // $10.00 + currency: 'usd', + recurring: { + interval: 'month', + trial_period_days: 14, + }, + }, + { + id: 'price_stub_pro_monthly', + product: { + id: 'prod_stub_pro', + name: 'Pro Plan', + description: 'For growing teams', + active: true, + }, + unit_amount: 2500, // $25.00 + currency: 'usd', + recurring: { + interval: 'month', + trial_period_days: 14, + }, + }, + { + id: 'price_stub_enterprise_monthly', + product: { + id: 'prod_stub_enterprise', + name: 'Enterprise Plan', + description: 'For large organizations', + active: true, + }, + unit_amount: 10000, // $100.00 + currency: 'usd', + recurring: { + interval: 'month', + trial_period_days: 14, + }, + }, + ], + }; + }, + }; + + webhooks = { + constructEvent: ( + payload: string, + signature: string, + secret: string + ): StubEvent => { + console.log('[Stripe Stub] Constructing webhook event'); + + // Parse the payload + const parsedPayload = JSON.parse(payload); + + return { + id: `evt_stub_${Date.now()}`, + type: parsedPayload.type || 'customer.subscription.updated', + data: { + object: parsedPayload.data?.object || { + id: `sub_stub_${Date.now()}`, + customer: `cus_stub_${Date.now()}`, + status: 'active', + items: { + data: [ + { + plan: { + product: { + id: 'prod_stub_basic', + name: 'Basic Plan', + }, + }, + }, + ], + }, + }, + }, + }; + }, + }; +} + +export const stripeStub = new StripeStub(); diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/payments/stripe.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/payments/stripe.ts new file mode 100644 index 000000000..2bb63b5fc --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/payments/stripe.ts @@ -0,0 +1,189 @@ +import Stripe from 'stripe'; +import { Team } from '@/lib/db/schema'; +import { + getTeamByStripeCustomerId, + updateTeamSubscription +} from '@/lib/db/queries'; +import { stripeStub } from './stripe-stub'; + +// Use stub if STRIPE_MODE=stub or if STRIPE_SECRET_KEY is missing/invalid +const useStub = + process.env.STRIPE_MODE === 'stub' || + !process.env.STRIPE_SECRET_KEY || + process.env.STRIPE_SECRET_KEY === 'sk_test_***'; + +export const stripe = useStub + ? (stripeStub as unknown as Stripe) + : new Stripe(process.env.STRIPE_SECRET_KEY!, { + apiVersion: '2025-08-27.basil' + }); + +if (useStub) { + console.log('🎭 Running in Stripe STUB mode - no real API calls will be made'); +} + +export async function createCheckoutSession({ + team, + priceId, + userId +}: { + team: Team; + priceId: string; + userId: number; +}) { + const session = await stripe.checkout.sessions.create({ + payment_method_types: ['card'], + line_items: [ + { + price: priceId, + quantity: 1 + } + ], + mode: 'subscription', + success_url: `${process.env.BASE_URL}/api/stripe/checkout?session_id={CHECKOUT_SESSION_ID}`, + cancel_url: `${process.env.BASE_URL}/pricing`, + customer: team.stripeCustomerId || undefined, + client_reference_id: userId.toString(), + allow_promotion_codes: true, + subscription_data: { + trial_period_days: 14 + } + }); + + return { url: session.url! }; +} + +export async function createCustomerPortalSession(team: Team) { + if (!team.stripeCustomerId || !team.stripeProductId) { + throw new Error('Team does not have a valid Stripe subscription'); + } + + let configuration: Stripe.BillingPortal.Configuration; + const configurations = await stripe.billingPortal.configurations.list(); + + if (configurations.data.length > 0) { + configuration = configurations.data[0]; + } else { + const product = await stripe.products.retrieve(team.stripeProductId); + if (!product.active) { + throw new Error("Team's product is not active in Stripe"); + } + + const prices = await stripe.prices.list({ + product: product.id, + active: true + }); + if (prices.data.length === 0) { + throw new Error("No active prices found for the team's product"); + } + + configuration = await stripe.billingPortal.configurations.create({ + business_profile: { + headline: 'Manage your subscription' + }, + features: { + subscription_update: { + enabled: true, + default_allowed_updates: ['price', 'quantity', 'promotion_code'], + proration_behavior: 'create_prorations', + products: [ + { + product: product.id, + prices: prices.data.map((price) => price.id) + } + ] + }, + subscription_cancel: { + enabled: true, + mode: 'at_period_end', + cancellation_reason: { + enabled: true, + options: [ + 'too_expensive', + 'missing_features', + 'switched_service', + 'unused', + 'other' + ] + } + }, + payment_method_update: { + enabled: true + } + } + }); + } + + return stripe.billingPortal.sessions.create({ + customer: team.stripeCustomerId, + return_url: `${process.env.BASE_URL}/dashboard`, + configuration: configuration.id + }); +} + +export async function handleSubscriptionChange( + subscription: Stripe.Subscription +) { + const customerId = subscription.customer as string; + const subscriptionId = subscription.id; + const status = subscription.status; + + const team = await getTeamByStripeCustomerId(customerId); + + if (!team) { + console.error('Team not found for Stripe customer:', customerId); + return; + } + + if (status === 'active' || status === 'trialing') { + const plan = subscription.items.data[0]?.plan; + await updateTeamSubscription(team.id, { + stripeSubscriptionId: subscriptionId, + stripeProductId: plan?.product as string, + planName: (plan?.product as Stripe.Product).name, + subscriptionStatus: status + }); + } else if (status === 'canceled' || status === 'unpaid') { + await updateTeamSubscription(team.id, { + stripeSubscriptionId: null, + stripeProductId: null, + planName: null, + subscriptionStatus: status + }); + } +} + +export async function getStripePrices() { + const prices = await stripe.prices.list({ + expand: ['data.product'], + active: true, + type: 'recurring' + }); + + return prices.data.map((price) => ({ + id: price.id, + productId: + typeof price.product === 'string' ? price.product : price.product.id, + unitAmount: price.unit_amount, + currency: price.currency, + interval: price.recurring?.interval, + trialPeriodDays: price.recurring?.trial_period_days + })); +} + +export async function getStripeProducts() { + const products = await stripe.products.list({ + active: true, + expand: ['data.default_price'] + }); + + return products.data.map((product) => ({ + id: product.id, + name: product.name, + description: product.description, + defaultPriceId: + typeof product.default_price === 'string' + ? product.default_price + : product.default_price?.id + })); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/utils.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/utils.ts new file mode 100644 index 000000000..bd0c391dd --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/middleware.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/middleware.ts new file mode 100644 index 000000000..1c8a638e4 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/middleware.ts @@ -0,0 +1,49 @@ +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; +import { signToken, verifyToken } from '@/lib/auth/session'; + +const protectedRoutes = '/dashboard'; + +export async function middleware(request: NextRequest) { + const { pathname } = request.nextUrl; + const sessionCookie = request.cookies.get('session'); + const isProtectedRoute = pathname.startsWith(protectedRoutes); + + if (isProtectedRoute && !sessionCookie) { + return NextResponse.redirect(new URL('/sign-in', request.url)); + } + + let res = NextResponse.next(); + + if (sessionCookie && request.method === 'GET') { + try { + const parsed = await verifyToken(sessionCookie.value); + const expiresInOneDay = new Date(Date.now() + 24 * 60 * 60 * 1000); + + res.cookies.set({ + name: 'session', + value: await signToken({ + ...parsed, + expires: expiresInOneDay.toISOString() + }), + httpOnly: true, + secure: true, + sameSite: 'lax', + expires: expiresInOneDay + }); + } catch (error) { + console.error('Error updating session:', error); + res.cookies.delete('session'); + if (isProtectedRoute) { + return NextResponse.redirect(new URL('/sign-in', request.url)); + } + } + } + + return res; +} + +export const config = { + matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], + runtime: 'nodejs' +}; diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/next.config.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/next.config.ts new file mode 100644 index 000000000..fd4fd4546 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/next.config.ts @@ -0,0 +1,9 @@ +import type { NextConfig } from 'next'; + +const nextConfig: NextConfig = { + // Configuration for stable Next.js 15 + // To enable experimental features like PPR, upgrade to canary: + // pnpm add next@canary +}; + +export default nextConfig; diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/package.json b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/package.json new file mode 100644 index 000000000..485799f45 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/package.json @@ -0,0 +1,43 @@ +{ + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "db:setup": "npx tsx lib/db/setup.ts", + "db:seed": "npx tsx lib/db/seed.ts", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:studio": "drizzle-kit studio" + }, + "dependencies": { + "@tailwindcss/postcss": "4.1.7", + "@types/node": "^22.15.18", + "@types/react": "19.1.4", + "@types/react-dom": "19.1.5", + "autoprefixer": "^10.4.21", + "bcryptjs": "^3.0.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "dotenv": "^16.5.0", + "drizzle-kit": "^0.31.1", + "drizzle-orm": "^0.43.1", + "jose": "^6.0.11", + "lucide-react": "^0.511.0", + "micro": "^10.0.1", + "next": "15.5.7", + "postcss": "^8.5.3", + "postgres": "^3.4.5", + "radix-ui": "^1.4.2", + "react": "19.1.2", + "react-dom": "19.1.2", + "server-only": "^0.0.1", + "stripe": "^18.1.0", + "swr": "^2.3.3", + "tailwind-merge": "^3.3.0", + "tailwindcss": "4.1.7", + "tw-animate-css": "^1.3.0", + "typescript": "^5.8.3", + "zod": "^3.24.4" + } +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/404.tsx b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/404.tsx new file mode 100644 index 000000000..78a642fbc --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/404.tsx @@ -0,0 +1,16 @@ +import Link from 'next/link'; +import { Button } from '@/components/ui/button'; + +export default function NotFound() { + return ( +
+
+

404

+

Page not found

+ +
+
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/_app.tsx b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/_app.tsx new file mode 100644 index 000000000..230860636 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/_app.tsx @@ -0,0 +1,20 @@ +import '@/styles/globals.css'; +import type { AppProps } from 'next/app'; +import { Manrope } from 'next/font/google'; +import { SWRConfig } from 'swr'; + +const manrope = Manrope({ subsets: ['latin'] }); + +export default function App({ Component, pageProps }: AppProps) { + return ( +
+ + + +
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/_document.tsx b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/_document.tsx new file mode 100644 index 000000000..eee6d9a05 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/_document.tsx @@ -0,0 +1,16 @@ +import { Html, Head, Main, NextScript } from 'next/document'; + +export default function Document() { + return ( + + + +
+ + + + ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/account/update.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/account/update.ts new file mode 100644 index 000000000..f0aa6ad31 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/account/update.ts @@ -0,0 +1,72 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { z } from 'zod'; +import { eq } from 'drizzle-orm'; +import { db } from '@/lib/db/drizzle'; +import { + users, + activityLogs, + type NewActivityLog, + ActivityType +} from '@/lib/db/schema'; +import { getUser, getUserWithTeam } from '@/lib/db/queries'; + +async function logActivity( + teamId: number | null | undefined, + userId: number, + type: ActivityType, + ipAddress?: string +) { + if (teamId === null || teamId === undefined) { + return; + } + const newActivity: NewActivityLog = { + teamId, + userId, + action: type, + ipAddress: ipAddress || '' + }; + await db.insert(activityLogs).values(newActivity); +} + +const updateAccountSchema = z.object({ + name: z.string().min(1, 'Name is required').max(100), + email: z.string().email('Invalid email address') +}); + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const sessionCookie = req.cookies.session; + const user = await getUser(sessionCookie); + + if (!user) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + const validation = updateAccountSchema.safeParse(req.body); + if (!validation.success) { + return res.status(400).json({ + error: validation.error.errors[0]?.message || 'Invalid input' + }); + } + + const { name, email } = validation.data; + const userWithTeam = await getUserWithTeam(user.id); + + await Promise.all([ + db.update(users).set({ name, email }).where(eq(users.id, user.id)), + logActivity(userWithTeam?.teamId, user.id, ActivityType.UPDATE_ACCOUNT) + ]); + + return res.status(200).json({ name, success: 'Account updated successfully.' }); + } catch (error) { + console.error('Update account error:', error); + return res.status(500).json({ error: 'Failed to update account' }); + } +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/auth/sign-in.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/auth/sign-in.ts new file mode 100644 index 000000000..fddfb3423 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/auth/sign-in.ts @@ -0,0 +1,112 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { z } from 'zod'; +import { eq } from 'drizzle-orm'; +import { db } from '@/lib/db/drizzle'; +import { + users, + teams, + teamMembers, + activityLogs, + type NewActivityLog, + ActivityType +} from '@/lib/db/schema'; +import { comparePasswords, setSession } from '@/lib/auth/session'; +import { createCheckoutSession } from '@/lib/payments/stripe'; + +async function logActivity( + teamId: number | null | undefined, + userId: number, + type: ActivityType, + ipAddress?: string +) { + if (teamId === null || teamId === undefined) { + return; + } + const newActivity: NewActivityLog = { + teamId, + userId, + action: type, + ipAddress: ipAddress || '' + }; + await db.insert(activityLogs).values(newActivity); +} + +const signInSchema = z.object({ + email: z.string().email().min(3).max(255), + password: z.string().min(8).max(100) +}); + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const { email, password, redirect, priceId } = req.body; + + const validation = signInSchema.safeParse({ email, password }); + if (!validation.success) { + return res.status(400).json({ + error: 'Invalid email or password format.', + email, + password + }); + } + + const userWithTeam = await db + .select({ + user: users, + team: teams + }) + .from(users) + .leftJoin(teamMembers, eq(users.id, teamMembers.userId)) + .leftJoin(teams, eq(teamMembers.teamId, teams.id)) + .where(eq(users.email, email)) + .limit(1); + + if (userWithTeam.length === 0) { + return res.status(401).json({ + error: 'Invalid email or password. Please try again.', + email, + password + }); + } + + const { user: foundUser, team: foundTeam } = userWithTeam[0]; + + const isPasswordValid = await comparePasswords( + password, + foundUser.passwordHash + ); + + if (!isPasswordValid) { + return res.status(401).json({ + error: 'Invalid email or password. Please try again.', + email, + password + }); + } + + await Promise.all([ + setSession(foundUser, res), + logActivity(foundTeam?.id, foundUser.id, ActivityType.SIGN_IN) + ]); + + if (redirect === 'checkout' && foundTeam) { + const checkoutResult = await createCheckoutSession({ + team: foundTeam, + priceId, + userId: foundUser.id + }); + return res.status(200).json(checkoutResult); + } + + return res.status(200).json({ success: true, redirectTo: '/dashboard' }); + } catch (error) { + console.error('Sign in error:', error); + return res.status(500).json({ error: 'Failed to sign in. Please try again.' }); + } +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/auth/sign-out.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/auth/sign-out.ts new file mode 100644 index 000000000..9ac5f24d1 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/auth/sign-out.ts @@ -0,0 +1,23 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + // Delete the session cookie by setting it with an expired date + res.setHeader( + 'Set-Cookie', + 'session=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT' + ); + + return res.status(200).json({ success: true }); + } catch (error) { + console.error('Sign out error:', error); + return res.status(500).json({ error: 'Failed to sign out' }); + } +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/auth/sign-up.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/auth/sign-up.ts new file mode 100644 index 000000000..10c2c0dcb --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/auth/sign-up.ts @@ -0,0 +1,183 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { z } from 'zod'; +import { and, eq } from 'drizzle-orm'; +import { db } from '@/lib/db/drizzle'; +import { + users, + teams, + teamMembers, + activityLogs, + invitations, + type NewUser, + type NewTeam, + type NewTeamMember, + type NewActivityLog, + ActivityType +} from '@/lib/db/schema'; +import { hashPassword, setSession } from '@/lib/auth/session'; +import { createCheckoutSession } from '@/lib/payments/stripe'; + +async function logActivity( + teamId: number | null | undefined, + userId: number, + type: ActivityType, + ipAddress?: string +) { + if (teamId === null || teamId === undefined) { + return; + } + const newActivity: NewActivityLog = { + teamId, + userId, + action: type, + ipAddress: ipAddress || '' + }; + await db.insert(activityLogs).values(newActivity); +} + +const signUpSchema = z.object({ + email: z.string().email(), + password: z.string().min(8), + inviteId: z.string().optional() +}); + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const { email, password, inviteId, redirect, priceId } = req.body; + + const validation = signUpSchema.safeParse({ email, password, inviteId }); + if (!validation.success) { + return res.status(400).json({ + error: 'Invalid input. Please check your email and password.', + email, + password + }); + } + + const existingUser = await db + .select() + .from(users) + .where(eq(users.email, email)) + .limit(1); + + if (existingUser.length > 0) { + return res.status(400).json({ + error: 'Failed to create user. Please try again.', + email, + password + }); + } + + const passwordHash = await hashPassword(password); + + const newUser: NewUser = { + email, + passwordHash, + role: 'owner' + }; + + const [createdUser] = await db.insert(users).values(newUser).returning(); + + if (!createdUser) { + return res.status(500).json({ + error: 'Failed to create user. Please try again.', + email, + password + }); + } + + let teamId: number; + let userRole: string; + let createdTeam: typeof teams.$inferSelect | null = null; + + if (inviteId) { + const [invitation] = await db + .select() + .from(invitations) + .where( + and( + eq(invitations.id, parseInt(inviteId)), + eq(invitations.email, email), + eq(invitations.status, 'pending') + ) + ) + .limit(1); + + if (invitation) { + teamId = invitation.teamId; + userRole = invitation.role; + + await db + .update(invitations) + .set({ status: 'accepted' }) + .where(eq(invitations.id, invitation.id)); + + await logActivity(teamId, createdUser.id, ActivityType.ACCEPT_INVITATION); + + [createdTeam] = await db + .select() + .from(teams) + .where(eq(teams.id, teamId)) + .limit(1); + } else { + return res.status(400).json({ + error: 'Invalid or expired invitation.', + email, + password + }); + } + } else { + const newTeam: NewTeam = { + name: `${email}'s Team` + }; + + [createdTeam] = await db.insert(teams).values(newTeam).returning(); + + if (!createdTeam) { + return res.status(500).json({ + error: 'Failed to create team. Please try again.', + email, + password + }); + } + + teamId = createdTeam.id; + userRole = 'owner'; + + await logActivity(teamId, createdUser.id, ActivityType.CREATE_TEAM); + } + + const newTeamMember: NewTeamMember = { + userId: createdUser.id, + teamId: teamId, + role: userRole + }; + + await Promise.all([ + db.insert(teamMembers).values(newTeamMember), + logActivity(teamId, createdUser.id, ActivityType.SIGN_UP), + setSession(createdUser, res) + ]); + + if (redirect === 'checkout' && createdTeam) { + const checkoutResult = await createCheckoutSession({ + team: createdTeam, + priceId, + userId: createdUser.id + }); + return res.status(200).json(checkoutResult); + } + + return res.status(200).json({ success: true, redirectTo: '/dashboard' }); + } catch (error) { + console.error('Sign up error:', error); + return res.status(500).json({ error: 'Failed to sign up. Please try again.' }); + } +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/stripe/checkout.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/stripe/checkout.ts new file mode 100644 index 000000000..9bdee729c --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/stripe/checkout.ts @@ -0,0 +1,103 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { eq } from 'drizzle-orm'; +import { db } from '@/lib/db/drizzle'; +import { users, teams, teamMembers } from '@/lib/db/schema'; +import { setSession } from '@/lib/auth/session'; +import { stripe } from '@/lib/payments/stripe'; +import Stripe from 'stripe'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + const { session_id } = req.query; + + if (!session_id || typeof session_id !== 'string') { + return res.redirect('/pricing'); + } + + try { + const session = await stripe.checkout.sessions.retrieve(session_id, { + expand: ['customer', 'subscription'] + }); + + if (!session.customer || typeof session.customer === 'string') { + throw new Error('Invalid customer data from Stripe.'); + } + + const customerId = session.customer.id; + const subscriptionId = + typeof session.subscription === 'string' + ? session.subscription + : session.subscription?.id; + + if (!subscriptionId) { + throw new Error('No subscription found for this session.'); + } + + const subscription = await stripe.subscriptions.retrieve(subscriptionId, { + expand: ['items.data.price.product'] + }); + + const plan = subscription.items.data[0]?.price; + + if (!plan) { + throw new Error('No plan found for this subscription.'); + } + + const productId = (plan.product as Stripe.Product).id; + + if (!productId) { + throw new Error('No product ID found for this subscription.'); + } + + const userId = session.client_reference_id; + if (!userId) { + throw new Error("No user ID found in session's client_reference_id."); + } + + const user = await db + .select() + .from(users) + .where(eq(users.id, Number(userId))) + .limit(1); + + if (user.length === 0) { + throw new Error('User not found in database.'); + } + + const userTeam = await db + .select({ + teamId: teamMembers.teamId + }) + .from(teamMembers) + .where(eq(teamMembers.userId, user[0].id)) + .limit(1); + + if (userTeam.length === 0) { + throw new Error('User is not associated with any team.'); + } + + await db + .update(teams) + .set({ + stripeCustomerId: customerId, + stripeSubscriptionId: subscriptionId, + stripeProductId: productId, + planName: (plan.product as Stripe.Product).name, + subscriptionStatus: subscription.status, + updatedAt: new Date() + }) + .where(eq(teams.id, userTeam[0].teamId)); + + await setSession(user[0]); + return res.redirect('/dashboard'); + } catch (error) { + console.error('Error handling successful checkout:', error); + return res.redirect('/error'); + } +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/stripe/create-checkout.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/stripe/create-checkout.ts new file mode 100644 index 000000000..789eb16fb --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/stripe/create-checkout.ts @@ -0,0 +1,33 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { createCheckoutSession } from '@/lib/payments/stripe'; +import { getUser, getTeamForUser } from '@/lib/db/queries'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const sessionCookie = req.cookies.session; + const priceId = req.body.priceId as string; + + const user = await getUser(sessionCookie); + const team = user ? await getTeamForUser(sessionCookie) : null; + + if (!team || !user) { + // Redirect to sign up if no team + return res.status(200).json({ + redirectTo: `/sign-up?redirect=checkout&priceId=${priceId}` + }); + } + + const result = await createCheckoutSession({ team, priceId, userId: user.id }); + return res.status(200).json(result); + } catch (error) { + console.error('Checkout error:', error); + return res.status(500).json({ error: 'Failed to create checkout session' }); + } +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/stripe/customer-portal.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/stripe/customer-portal.ts new file mode 100644 index 000000000..91302967f --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/stripe/customer-portal.ts @@ -0,0 +1,33 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { createCustomerPortalSession } from '@/lib/payments/stripe'; +import { getUser, getTeamForUser } from '@/lib/db/queries'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const sessionCookie = req.cookies.session; + const user = await getUser(sessionCookie); + + if (!user) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + const team = await getTeamForUser(sessionCookie); + + if (!team) { + return res.status(400).json({ error: 'Team not found' }); + } + + const portalSession = await createCustomerPortalSession(team); + return res.status(200).json({ url: portalSession.url }); + } catch (error) { + console.error('Customer portal error:', error); + return res.status(500).json({ error: 'Failed to create customer portal session' }); + } +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/stripe/webhook.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/stripe/webhook.ts new file mode 100644 index 000000000..81cee0399 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/stripe/webhook.ts @@ -0,0 +1,50 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import Stripe from 'stripe'; +import { handleSubscriptionChange, stripe } from '@/lib/payments/stripe'; +import { buffer } from 'micro'; + +// Disable body parsing, need raw body for Stripe webhook signature verification +export const config = { + api: { + bodyParser: false + } +}; + +const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET || 'whsec_stub_secret'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + const buf = await buffer(req); + const signature = req.headers['stripe-signature'] as string; + + let event: Stripe.Event; + + try { + event = stripe.webhooks.constructEvent( + buf.toString(), + signature, + webhookSecret + ); + } catch (err) { + console.error('Webhook signature verification failed.', err); + return res.status(400).json({ error: 'Webhook signature verification failed.' }); + } + + switch (event.type) { + case 'customer.subscription.updated': + case 'customer.subscription.deleted': + const subscription = event.data.object as Stripe.Subscription; + await handleSubscriptionChange(subscription); + break; + default: + console.log(`Unhandled event type ${event.type}`); + } + + return res.status(200).json({ received: true }); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/team.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/team.ts new file mode 100644 index 000000000..5f345f70e --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/team.ts @@ -0,0 +1,19 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getTeamForUser } from '@/lib/db/queries'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const sessionCookie = req.cookies.session; + const team = await getTeamForUser(sessionCookie); + return res.status(200).json(team); + } catch (error) { + return res.status(500).json({ error: 'Failed to fetch team' }); + } +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/team/invite.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/team/invite.ts new file mode 100644 index 000000000..677c799de --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/team/invite.ts @@ -0,0 +1,116 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { z } from 'zod'; +import { and, eq } from 'drizzle-orm'; +import { db } from '@/lib/db/drizzle'; +import { + users, + teamMembers, + activityLogs, + invitations, + type NewActivityLog, + ActivityType +} from '@/lib/db/schema'; +import { getUser, getUserWithTeam } from '@/lib/db/queries'; + +async function logActivity( + teamId: number | null | undefined, + userId: number, + type: ActivityType, + ipAddress?: string +) { + if (teamId === null || teamId === undefined) { + return; + } + const newActivity: NewActivityLog = { + teamId, + userId, + action: type, + ipAddress: ipAddress || '' + }; + await db.insert(activityLogs).values(newActivity); +} + +const inviteTeamMemberSchema = z.object({ + email: z.string().email('Invalid email address'), + role: z.enum(['member', 'owner']) +}); + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const sessionCookie = req.cookies.session; + const user = await getUser(sessionCookie); + + if (!user) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + const validation = inviteTeamMemberSchema.safeParse(req.body); + if (!validation.success) { + return res.status(400).json({ + error: validation.error.errors[0]?.message || 'Invalid input' + }); + } + + const { email, role } = validation.data; + const userWithTeam = await getUserWithTeam(user.id); + + if (!userWithTeam?.teamId) { + return res.status(400).json({ error: 'User is not part of a team' }); + } + + const existingMember = await db + .select() + .from(users) + .leftJoin(teamMembers, eq(users.id, teamMembers.userId)) + .where( + and(eq(users.email, email), eq(teamMembers.teamId, userWithTeam.teamId)) + ) + .limit(1); + + if (existingMember.length > 0) { + return res.status(400).json({ error: 'User is already a member of this team' }); + } + + const existingInvitation = await db + .select() + .from(invitations) + .where( + and( + eq(invitations.email, email), + eq(invitations.teamId, userWithTeam.teamId), + eq(invitations.status, 'pending') + ) + ) + .limit(1); + + if (existingInvitation.length > 0) { + return res.status(400).json({ error: 'An invitation has already been sent to this email' }); + } + + await db.insert(invitations).values({ + teamId: userWithTeam.teamId, + email, + role, + invitedBy: user.id, + status: 'pending' + }); + + await logActivity( + userWithTeam.teamId, + user.id, + ActivityType.INVITE_TEAM_MEMBER + ); + + return res.status(200).json({ success: 'Invitation sent successfully' }); + } catch (error) { + console.error('Invite team member error:', error); + return res.status(500).json({ error: 'Failed to invite team member' }); + } +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/team/remove-member.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/team/remove-member.ts new file mode 100644 index 000000000..012e7c9c6 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/team/remove-member.ts @@ -0,0 +1,85 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { z } from 'zod'; +import { and, eq } from 'drizzle-orm'; +import { db } from '@/lib/db/drizzle'; +import { + teamMembers, + activityLogs, + type NewActivityLog, + ActivityType +} from '@/lib/db/schema'; +import { getUser, getUserWithTeam } from '@/lib/db/queries'; + +async function logActivity( + teamId: number | null | undefined, + userId: number, + type: ActivityType, + ipAddress?: string +) { + if (teamId === null || teamId === undefined) { + return; + } + const newActivity: NewActivityLog = { + teamId, + userId, + action: type, + ipAddress: ipAddress || '' + }; + await db.insert(activityLogs).values(newActivity); +} + +const removeTeamMemberSchema = z.object({ + memberId: z.number() +}); + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const sessionCookie = req.cookies.session; + const user = await getUser(sessionCookie); + + if (!user) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + const validation = removeTeamMemberSchema.safeParse(req.body); + if (!validation.success) { + return res.status(400).json({ + error: validation.error.errors[0]?.message || 'Invalid input' + }); + } + + const { memberId } = validation.data; + const userWithTeam = await getUserWithTeam(user.id); + + if (!userWithTeam?.teamId) { + return res.status(400).json({ error: 'User is not part of a team' }); + } + + await db + .delete(teamMembers) + .where( + and( + eq(teamMembers.id, memberId), + eq(teamMembers.teamId, userWithTeam.teamId) + ) + ); + + await logActivity( + userWithTeam.teamId, + user.id, + ActivityType.REMOVE_TEAM_MEMBER + ); + + return res.status(200).json({ success: 'Team member removed successfully' }); + } catch (error) { + console.error('Remove team member error:', error); + return res.status(500).json({ error: 'Failed to remove team member' }); + } +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/user.ts b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/user.ts new file mode 100644 index 000000000..c1e24fda5 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/api/user.ts @@ -0,0 +1,19 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getUser } from '@/lib/db/queries'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const sessionCookie = req.cookies.session; + const user = await getUser(sessionCookie); + return res.status(200).json(user); + } catch (error) { + return res.status(500).json({ error: 'Failed to fetch user' }); + } +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/dashboard/activity.tsx b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/dashboard/activity.tsx new file mode 100644 index 000000000..fb749da8b --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/dashboard/activity.tsx @@ -0,0 +1,53 @@ +import { GetServerSideProps } from 'next'; +import { DashboardLayout } from '@/components/dashboard-layout'; +import { verifyToken } from '@/lib/auth/session'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; + +export default function ActivityPage() { + return ( + +
+

+ Activity Log +

+ + + Recent Activity + + +

View your recent account activity here.

+
+
+
+
+ ); +} + +export const getServerSideProps: GetServerSideProps = async (context) => { + const { req } = context; + const sessionCookie = req.cookies.session; + + if (!sessionCookie) { + return { + redirect: { + destination: '/sign-in', + permanent: false + } + }; + } + + try { + await verifyToken(sessionCookie); + } catch (error) { + return { + redirect: { + destination: '/sign-in', + permanent: false + } + }; + } + + return { + props: {} + }; +}; diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/dashboard/general.tsx b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/dashboard/general.tsx new file mode 100644 index 000000000..74d0dc0e5 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/dashboard/general.tsx @@ -0,0 +1,156 @@ +import { GetServerSideProps } from 'next'; +import { DashboardLayout } from '@/components/dashboard-layout'; +import { verifyToken } from '@/lib/auth/session'; +import { + Card, + CardContent, + CardHeader, + CardTitle +} from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Loader2 } from 'lucide-react'; +import useSWR from 'swr'; +import { User } from '@/lib/db/schema'; +import { useState, useTransition } from 'react'; + +const fetcher = (url: string) => fetch(url).then((res) => res.json()); + +export default function GeneralPage() { + const { data: user } = useSWR('/api/user', fetcher); + const [isPending, startTransition] = useTransition(); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + const [name, setName] = useState(''); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(''); + setSuccess(''); + + const formData = new FormData(e.currentTarget); + const data = { + name: formData.get('name') as string, + email: formData.get('email') as string + }; + + startTransition(async () => { + try { + const response = await fetch('/api/account/update', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + + const result = await response.json(); + + if (!response.ok) { + setError(result.error || 'An error occurred'); + return; + } + + setSuccess(result.success); + setName(result.name); + } catch (err) { + setError('An unexpected error occurred. Please try again.'); + } + }); + } + + return ( + +
+

+ General Settings +

+ + + + Account Information + + +
+
+ + +
+
+ + +
+ {error && ( +

{error}

+ )} + {success && ( +

{success}

+ )} + +
+
+
+
+
+ ); +} + +export const getServerSideProps: GetServerSideProps = async (context) => { + const { req } = context; + const sessionCookie = req.cookies.session; + + if (!sessionCookie) { + return { + redirect: { + destination: '/sign-in', + permanent: false + } + }; + } + + try { + await verifyToken(sessionCookie); + } catch (error) { + return { + redirect: { + destination: '/sign-in', + permanent: false + } + }; + } + + return { + props: {} + }; +}; diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/dashboard/index.tsx b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/dashboard/index.tsx new file mode 100644 index 000000000..cc528f768 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/dashboard/index.tsx @@ -0,0 +1,341 @@ +import { GetServerSideProps } from 'next'; +import { DashboardLayout } from '@/components/dashboard-layout'; +import { getUser, getTeamForUser } from '@/lib/db/queries'; +import { User, TeamDataWithMembers } from '@/lib/db/schema'; +import { verifyToken } from '@/lib/auth/session'; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle +} from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { Avatar, AvatarFallback } from '@/components/ui/avatar'; +import { Loader2, PlusCircle } from 'lucide-react'; +import useSWR, { mutate } from 'swr'; +import { useState, useTransition } from 'react'; + +const fetcher = (url: string) => fetch(url).then((res) => res.json()); + +function ManageSubscription() { + const { data: teamData } = useSWR('/api/team', fetcher); + + async function handleManageSubscription() { + try { + const response = await fetch('/api/stripe/customer-portal', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + + const result = await response.json(); + + if (response.ok && result.url) { + window.location.href = result.url; + } + } catch (err) { + console.error('Failed to open customer portal'); + } + } + + return ( + + + Team Subscription + + +
+
+
+

+ Current Plan: {teamData?.planName || 'Free'} +

+

+ {teamData?.subscriptionStatus === 'active' + ? 'Billed monthly' + : teamData?.subscriptionStatus === 'trialing' + ? 'Trial period' + : 'No active subscription'} +

+
+ +
+
+
+
+ ); +} + +function TeamMembers() { + const { data: teamData } = useSWR('/api/team', fetcher); + const [isPending, startTransition] = useTransition(); + const [error, setError] = useState(''); + + const getUserDisplayName = (user: Pick) => { + return user.name || user.email || 'Unknown User'; + }; + + async function handleRemoveMember(memberId: number) { + setError(''); + + startTransition(async () => { + try { + const response = await fetch('/api/team/remove-member', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ memberId }) + }); + + const result = await response.json(); + + if (!response.ok) { + setError(result.error || 'Failed to remove member'); + return; + } + + // Refresh team data + mutate('/api/team'); + } catch (err) { + setError('An unexpected error occurred'); + } + }); + } + + if (!teamData?.teamMembers?.length) { + return ( + + + Team Members + + +

No team members yet.

+
+
+ ); + } + + return ( + + + Team Members + + +
    + {teamData.teamMembers.map((member, index) => ( +
  • +
    + + + {getUserDisplayName(member.user) + .split(' ') + .map((n) => n[0]) + .join('')} + + +
    +

    + {getUserDisplayName(member.user)} +

    +

    + {member.role} +

    +
    +
    + {index > 1 ? ( + + ) : null} +
  • + ))} +
+ {error && ( +

{error}

+ )} +
+
+ ); +} + +function InviteTeamMember() { + const { data: user } = useSWR('/api/user', fetcher); + const isOwner = user?.role === 'owner'; + const [isPending, startTransition] = useTransition(); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(''); + setSuccess(''); + + const formData = new FormData(e.currentTarget); + const data = { + email: formData.get('email') as string, + role: formData.get('role') as 'member' | 'owner' + }; + + startTransition(async () => { + try { + const response = await fetch('/api/team/invite', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + + const result = await response.json(); + + if (!response.ok) { + setError(result.error || 'Failed to send invitation'); + return; + } + + setSuccess(result.success); + // Reset form + (e.target as HTMLFormElement).reset(); + } catch (err) { + setError('An unexpected error occurred'); + } + }); + } + + return ( + + + Invite Team Member + + +
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+
+ {error && ( +

{error}

+ )} + {success && ( +

{success}

+ )} + +
+
+ {!isOwner && ( + +

+ You must be a team owner to invite new members. +

+
+ )} +
+ ); +} + +export default function DashboardPage() { + return ( + +
+

Team Settings

+ + + +
+
+ ); +} + +export const getServerSideProps: GetServerSideProps = async (context) => { + const { req } = context; + const sessionCookie = req.cookies.session; + + if (!sessionCookie) { + return { + redirect: { + destination: '/sign-in', + permanent: false + } + }; + } + + try { + await verifyToken(sessionCookie); + } catch (error) { + return { + redirect: { + destination: '/sign-in', + permanent: false + } + }; + } + + const user = await getUser(sessionCookie); + const team = user ? await getTeamForUser(sessionCookie) : null; + + return { + props: { + fallback: { + '/api/user': user ? JSON.parse(JSON.stringify(user)) : null, + '/api/team': team ? JSON.parse(JSON.stringify(team)) : null + } + } + }; +}; diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/dashboard/security.tsx b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/dashboard/security.tsx new file mode 100644 index 000000000..03b2f18e8 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/dashboard/security.tsx @@ -0,0 +1,53 @@ +import { GetServerSideProps } from 'next'; +import { DashboardLayout } from '@/components/dashboard-layout'; +import { verifyToken } from '@/lib/auth/session'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; + +export default function SecurityPage() { + return ( + +
+

+ Security Settings +

+ + + Password & Security + + +

Manage your password and security settings here.

+
+
+
+
+ ); +} + +export const getServerSideProps: GetServerSideProps = async (context) => { + const { req } = context; + const sessionCookie = req.cookies.session; + + if (!sessionCookie) { + return { + redirect: { + destination: '/sign-in', + permanent: false + } + }; + } + + try { + await verifyToken(sessionCookie); + } catch (error) { + return { + redirect: { + destination: '/sign-in', + permanent: false + } + }; + } + + return { + props: {} + }; +}; diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/index.tsx b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/index.tsx new file mode 100644 index 000000000..dba57f706 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/index.tsx @@ -0,0 +1,159 @@ +import { Button } from '@/components/ui/button'; +import { ArrowRight, CreditCard, Database } from 'lucide-react'; +import { Terminal } from '@/components/terminal'; +import { Header } from '@/components/header'; +import { GetServerSideProps } from 'next'; +import { getUser, getTeamForUser } from '@/lib/db/queries'; +import { User, TeamDataWithMembers } from '@/lib/db/schema'; + +interface HomePageProps { + fallback: { + '/api/user': User | null; + '/api/team': TeamDataWithMembers | null; + }; +} + +export default function HomePage({ fallback }: HomePageProps) { + return ( +
+
+
+
+
+
+
+

+ Build Your SaaS + Faster Than Ever +

+

+ Launch your SaaS product in record time with our powerful, + ready-to-use template. Packed with modern technologies and + essential integrations. +

+ +
+
+ +
+
+
+
+ +
+
+
+
+
+ + + +
+
+

+ Next.js and React +

+

+ Leverage the power of modern web technologies for optimal + performance and developer experience. +

+
+
+ +
+
+ +
+
+

+ Postgres and Drizzle ORM +

+

+ Robust database solution with an intuitive ORM for efficient + data management and scalability. +

+
+
+ +
+
+ +
+
+

+ Stripe Integration +

+

+ Seamless payment processing and subscription management with + industry-leading Stripe integration. +

+
+
+
+
+
+ +
+
+
+
+

+ Ready to launch your SaaS? +

+

+ Our template provides everything you need to get your SaaS up + and running quickly. Don't waste time on boilerplate - focus on + what makes your product unique. +

+
+ +
+
+
+
+
+ ); +} + +export const getServerSideProps: GetServerSideProps = async (context) => { + const sessionCookie = context.req.cookies.session; + const user = await getUser(sessionCookie); + const team = user ? await getTeamForUser(sessionCookie) : null; + + return { + props: { + fallback: { + '/api/user': user ? JSON.parse(JSON.stringify(user)) : null, + '/api/team': team ? JSON.parse(JSON.stringify(team)) : null + } + } + }; +}; diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/pricing.tsx b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/pricing.tsx new file mode 100644 index 000000000..0067ec8ed --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/pricing.tsx @@ -0,0 +1,194 @@ +import { GetServerSideProps } from 'next'; +import { Check, ArrowRight, Loader2 } from 'lucide-react'; +import { getStripePrices, getStripeProducts } from '@/lib/payments/stripe'; +import { Header } from '@/components/header'; +import { Button } from '@/components/ui/button'; +import { useState, useTransition } from 'react'; +import { useRouter } from 'next/router'; +import { getUser, getTeamForUser } from '@/lib/db/queries'; +import { User, TeamDataWithMembers } from '@/lib/db/schema'; + +interface Price { + id: string; + productId: string; + unitAmount: number; + interval: string; + trialPeriodDays: number; +} + +interface Product { + id: string; + name: string; +} + +interface PricingPageProps { + prices: Price[]; + products: Product[]; + fallback: { + '/api/user': User | null; + '/api/team': TeamDataWithMembers | null; + }; +} + +function SubmitButton({ isPending }: { isPending: boolean }) { + return ( + + ); +} + +function PricingCard({ + name, + price, + interval, + trialDays, + features, + priceId +}: { + name: string; + price: number; + interval: string; + trialDays: number; + features: string[]; + priceId?: string; +}) { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + + startTransition(async () => { + try { + const response = await fetch('/api/stripe/create-checkout', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ priceId }) + }); + + const result = await response.json(); + + if (result.redirectTo) { + router.push(result.redirectTo); + } else if (result.url) { + window.location.href = result.url; + } + } catch (err) { + console.error('Checkout error:', err); + } + }); + } + + return ( +
+

{name}

+

+ with {trialDays} day free trial +

+

+ ${price / 100}{' '} + + per user / {interval} + +

+
    + {features.map((feature, index) => ( +
  • + + {feature} +
  • + ))} +
+
+ + + +
+ ); +} + +export default function PricingPage({ + prices, + products, + fallback +}: PricingPageProps) { + const basePlan = products.find((product) => product.name === 'Base'); + const plusPlan = products.find((product) => product.name === 'Plus'); + + const basePrice = prices.find((price) => price.productId === basePlan?.id); + const plusPrice = prices.find((price) => price.productId === plusPlan?.id); + + return ( +
+
+
+
+ + +
+
+
+ ); +} + +export const getServerSideProps: GetServerSideProps = async (context) => { + const sessionCookie = context.req.cookies.session; + const [prices, products, user] = await Promise.all([ + getStripePrices(), + getStripeProducts(), + getUser(sessionCookie) + ]); + + const team = user ? await getTeamForUser(sessionCookie) : null; + + return { + props: { + prices, + products, + fallback: { + '/api/user': user ? JSON.parse(JSON.stringify(user)) : null, + '/api/team': team ? JSON.parse(JSON.stringify(team)) : null + } + } + }; +}; diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/sign-in.tsx b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/sign-in.tsx new file mode 100644 index 000000000..e1d79b23a --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/sign-in.tsx @@ -0,0 +1,48 @@ +import { GetServerSideProps } from 'next'; +import { Login } from '@/components/login'; +import { verifyToken } from '@/lib/auth/session'; + +interface SignInPageProps { + redirect?: string | null; + priceId?: string | null; +} + +export default function SignInPage({ redirect, priceId }: SignInPageProps) { + return ( + + ); +} + +export const getServerSideProps: GetServerSideProps = async (context) => { + const { req, query } = context; + const sessionCookie = req.cookies.session; + + // If already signed in, redirect to dashboard + if (sessionCookie) { + try { + await verifyToken(sessionCookie); + return { + redirect: { + destination: '/dashboard', + permanent: false + } + }; + } catch (error) { + // Invalid session, continue to login page + } + } + + const redirect = query.redirect as string | undefined; + const priceId = query.priceId as string | undefined; + + return { + props: { + redirect: redirect || null, + priceId: priceId || null + } + }; +}; diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/sign-up.tsx b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/sign-up.tsx new file mode 100644 index 000000000..aa702a64f --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pages/sign-up.tsx @@ -0,0 +1,56 @@ +import { GetServerSideProps } from 'next'; +import { Login } from '@/components/login'; +import { verifyToken } from '@/lib/auth/session'; + +interface SignUpPageProps { + redirect?: string | null; + priceId?: string | null; + inviteId?: string | null; +} + +export default function SignUpPage({ + redirect, + priceId, + inviteId +}: SignUpPageProps) { + return ( + + ); +} + +export const getServerSideProps: GetServerSideProps = async (context) => { + const { req, query } = context; + const sessionCookie = req.cookies.session; + + // If already signed in, redirect to dashboard + if (sessionCookie) { + try { + await verifyToken(sessionCookie); + return { + redirect: { + destination: '/dashboard', + permanent: false + } + }; + } catch (error) { + // Invalid session, continue to signup page + } + } + + const redirect = query.redirect as string | undefined; + const priceId = query.priceId as string | undefined; + const inviteId = query.inviteId as string | undefined; + + return { + props: { + redirect: redirect || null, + priceId: priceId || null, + inviteId: inviteId || null + } + }; +}; diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pnpm-lock.yaml b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pnpm-lock.yaml new file mode 100644 index 000000000..693ff8558 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/pnpm-lock.yaml @@ -0,0 +1,3834 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@tailwindcss/postcss': + specifier: 4.1.7 + version: 4.1.7 + '@types/node': + specifier: ^22.15.18 + version: 22.19.1 + '@types/react': + specifier: 19.1.4 + version: 19.1.4 + '@types/react-dom': + specifier: 19.1.5 + version: 19.1.5(@types/react@19.1.4) + autoprefixer: + specifier: ^10.4.21 + version: 10.4.22(postcss@8.5.6) + bcryptjs: + specifier: ^3.0.2 + version: 3.0.3 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + dotenv: + specifier: ^16.5.0 + version: 16.6.1 + drizzle-kit: + specifier: ^0.31.1 + version: 0.31.7 + drizzle-orm: + specifier: ^0.43.1 + version: 0.43.1(postgres@3.4.7) + jose: + specifier: ^6.0.11 + version: 6.1.3 + lucide-react: + specifier: ^0.511.0 + version: 0.511.0(react@19.1.2) + micro: + specifier: ^10.0.1 + version: 10.0.1 + next: + specifier: 15.5.7 + version: 15.5.7(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + postcss: + specifier: ^8.5.3 + version: 8.5.6 + postgres: + specifier: ^3.4.5 + version: 3.4.7 + radix-ui: + specifier: ^1.4.2 + version: 1.4.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: + specifier: 19.1.2 + version: 19.1.2 + react-dom: + specifier: 19.1.2 + version: 19.1.2(react@19.1.2) + server-only: + specifier: ^0.0.1 + version: 0.0.1 + stripe: + specifier: ^18.1.0 + version: 18.5.0(@types/node@22.19.1) + swr: + specifier: ^2.3.3 + version: 2.3.7(react@19.1.2) + tailwind-merge: + specifier: ^3.3.0 + version: 3.4.0 + tailwindcss: + specifier: 4.1.7 + version: 4.1.7 + tw-animate-css: + specifier: ^1.3.0 + version: 1.4.0 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + zod: + specifier: ^3.24.4 + version: 3.25.76 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + + '@emnapi/runtime@1.7.1': + resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + + '@floating-ui/react-dom@2.1.6': + resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@next/env@15.5.7': + resolution: {integrity: sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==} + + '@next/swc-darwin-arm64@15.5.7': + resolution: {integrity: sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@15.5.7': + resolution: {integrity: sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@15.5.7': + resolution: {integrity: sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@15.5.7': + resolution: {integrity: sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@15.5.7': + resolution: {integrity: sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@15.5.7': + resolution: {integrity: sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@15.5.7': + resolution: {integrity: sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@15.5.7': + resolution: {integrity: sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-accessible-icon@1.1.7': + resolution: {integrity: sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-accordion@1.2.12': + resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-alert-dialog@1.1.15': + resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-aspect-ratio@1.1.7': + resolution: {integrity: sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.1.10': + resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context-menu@2.2.16': + resolution: {integrity: sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.16': + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-form@0.1.8': + resolution: {integrity: sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-hover-card@1.1.15': + resolution: {integrity: sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.7': + resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.16': + resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menubar@1.1.16': + resolution: {integrity: sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-navigation-menu@1.2.14': + resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-one-time-password-field@0.1.8': + resolution: {integrity: sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-password-toggle-field@0.1.3': + resolution: {integrity: sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-progress@1.1.7': + resolution: {integrity: sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.3.8': + resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.10': + resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.7': + resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slider@1.3.6': + resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toast@1.2.15': + resolution: {integrity: sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle-group@1.1.11': + resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toolbar@1.1.11': + resolution: {integrity: sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-is-hydrated@0.1.0': + resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tailwindcss/node@4.1.7': + resolution: {integrity: sha512-9rsOpdY9idRI2NH6CL4wORFY0+Q6fnx9XP9Ju+iq/0wJwGD5IByIgFmwVbyy4ymuyprj8Qh4ErxMKTUL4uNh3g==} + + '@tailwindcss/oxide-android-arm64@4.1.7': + resolution: {integrity: sha512-IWA410JZ8fF7kACus6BrUwY2Z1t1hm0+ZWNEzykKmMNM09wQooOcN/VXr0p/WJdtHZ90PvJf2AIBS/Ceqx1emg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.7': + resolution: {integrity: sha512-81jUw9To7fimGGkuJ2W5h3/oGonTOZKZ8C2ghm/TTxbwvfSiFSDPd6/A/KE2N7Jp4mv3Ps9OFqg2fEKgZFfsvg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.7': + resolution: {integrity: sha512-q77rWjEyGHV4PdDBtrzO0tgBBPlQWKY7wZK0cUok/HaGgbNKecegNxCGikuPJn5wFAlIywC3v+WMBt0PEBtwGw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.7': + resolution: {integrity: sha512-RfmdbbK6G6ptgF4qqbzoxmH+PKfP4KSVs7SRlTwcbRgBwezJkAO3Qta/7gDy10Q2DcUVkKxFLXUQO6J3CRvBGw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.7': + resolution: {integrity: sha512-OZqsGvpwOa13lVd1z6JVwQXadEobmesxQ4AxhrwRiPuE04quvZHWn/LnihMg7/XkN+dTioXp/VMu/p6A5eZP3g==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.7': + resolution: {integrity: sha512-voMvBTnJSfKecJxGkoeAyW/2XRToLZ227LxswLAwKY7YslG/Xkw9/tJNH+3IVh5bdYzYE7DfiaPbRkSHFxY1xA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.7': + resolution: {integrity: sha512-PjGuNNmJeKHnP58M7XyjJyla8LPo+RmwHQpBI+W/OxqrwojyuCQ+GUtygu7jUqTEexejZHr/z3nBc/gTiXBj4A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.7': + resolution: {integrity: sha512-HMs+Va+ZR3gC3mLZE00gXxtBo3JoSQxtu9lobbZd+DmfkIxR54NO7Z+UQNPsa0P/ITn1TevtFxXTpsRU7qEvWg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.7': + resolution: {integrity: sha512-MHZ6jyNlutdHH8rd+YTdr3QbXrHXqwIhHw9e7yXEBcQdluGwhpQY2Eku8UZK6ReLaWtQ4gijIv5QoM5eE+qlsA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.7': + resolution: {integrity: sha512-ANaSKt74ZRzE2TvJmUcbFQ8zS201cIPxUDm5qez5rLEwWkie2SkGtA4P+GPTj+u8N6JbPrC8MtY8RmJA35Oo+A==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.7': + resolution: {integrity: sha512-HUiSiXQ9gLJBAPCMVRk2RT1ZrBjto7WvqsPBwUrNK2BcdSxMnk19h4pjZjI7zgPhDxlAbJSumTC4ljeA9y0tEw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.7': + resolution: {integrity: sha512-rYHGmvoHiLJ8hWucSfSOEmdCBIGZIq7SpkPRSqLsH2Ab2YUNgKeAPT1Fi2cx3+hnYOrAb0jp9cRyode3bBW4mQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.7': + resolution: {integrity: sha512-5SF95Ctm9DFiUyjUPnDGkoKItPX/k+xifcQhcqX5RA85m50jw1pT/KzjdvlqxRja45Y52nR4MR9fD1JYd7f8NQ==} + engines: {node: '>= 10'} + + '@tailwindcss/postcss@4.1.7': + resolution: {integrity: sha512-88g3qmNZn7jDgrrcp3ZXEQfp9CVox7xjP1HN2TFKI03CltPVd/c61ydn5qJJL8FYunn0OqBaW5HNUga0kmPVvw==} + + '@types/node@22.19.1': + resolution: {integrity: sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==} + + '@types/react-dom@19.1.5': + resolution: {integrity: sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==} + peerDependencies: + '@types/react': ^19.0.0 + + '@types/react@19.1.4': + resolution: {integrity: sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==} + + arg@4.1.0: + resolution: {integrity: sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + autoprefixer@10.4.22: + resolution: {integrity: sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + baseline-browser-mapping@2.9.0: + resolution: {integrity: sha512-Mh++g+2LPfzZToywfE1BUzvZbfOY52Nil0rn9H1CPC5DJ7fX+Vir7nToBeoiSbB1zTNeGYbELEvJESujgGrzXw==} + hasBin: true + + bcryptjs@3.0.3: + resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==} + hasBin: true + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + bytes@3.1.0: + resolution: {integrity: sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + caniuse-lite@1.0.30001759: + resolution: {integrity: sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + content-type@1.0.4: + resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==} + engines: {node: '>= 0.6'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + drizzle-kit@0.31.7: + resolution: {integrity: sha512-hOzRGSdyKIU4FcTSFYGKdXEjFsncVwHZ43gY3WU5Bz9j5Iadp6Rh6hxLSQ1IWXpKLBKt/d5y1cpSPcV+FcoQ1A==} + hasBin: true + + drizzle-orm@0.43.1: + resolution: {integrity: sha512-dUcDaZtE/zN4RV/xqGrVSMpnEczxd5cIaoDeor7Zst9wOe/HzC/7eAaulywWGYXdDEc9oBPMjayVEDg0ziTLJA==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1.13' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.263: + resolution: {integrity: sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg==} + + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + http-errors@1.7.3: + resolution: {integrity: sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==} + engines: {node: '>= 0.6'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.1: + resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + engines: {node: '>= 12.0.0'} + + lucide-react@0.511.0: + resolution: {integrity: sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + micro@10.0.1: + resolution: {integrity: sha512-9uwZSsUrqf6+4FLLpiPj5TRWQv5w5uJrJwsx1LR/TjqvQmKC1XnGQ9OHrFwR3cbZ46YqPqxO/XJCOpWnqMPw2Q==} + engines: {node: '>= 16.0.0'} + hasBin: true + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + next@15.5.7: + resolution: {integrity: sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + postgres@3.4.7: + resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} + engines: {node: '>=12'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + radix-ui@1.4.3: + resolution: {integrity: sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + raw-body@2.4.1: + resolution: {integrity: sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==} + engines: {node: '>= 0.8'} + + react-dom@19.1.2: + resolution: {integrity: sha512-dEoydsCp50i7kS1xHOmPXq4zQYoGWedUsvqv9H6zdif2r7yLHygyfP9qou71TulRN0d6ng9EbRVsQhSqfUc19g==} + peerDependencies: + react: ^19.1.2 + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react@19.1.2: + resolution: {integrity: sha512-MdWVitvLbQULD+4DP8GYjZUrepGW7d+GQkNVqJEzNxE+e9WIa4egVFE/RDfVb1u9u/Jw7dNMmPB4IqxzbFYJ0w==} + engines: {node: '>=0.10.0'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + server-only@0.0.1: + resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + + setprototypeof@1.1.1: + resolution: {integrity: sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==} + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + + stripe@18.5.0: + resolution: {integrity: sha512-Hp+wFiEQtCB0LlNgcFh5uVyKznpDjzyUZ+CNVEf+I3fhlYvh7rZruIg+jOwzJRCpy0ZTPMjlzm7J2/M2N6d+DA==} + engines: {node: '>=12.*'} + peerDependencies: + '@types/node': '>=12.x.x' + peerDependenciesMeta: + '@types/node': + optional: true + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + swr@2.3.7: + resolution: {integrity: sha512-ZEquQ82QvalqTxhBVv/DlAg2mbmUjF4UgpPg9wwk4ufb9rQnZXh1iKyyKBqV6bQGu1Ie7L1QwSYO07qFIa1p+g==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + tailwind-merge@3.4.0: + resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} + + tailwindcss@4.1.7: + resolution: {integrity: sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + tar@7.5.2: + resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} + engines: {node: '>=18'} + + toidentifier@1.0.0: + resolution: {integrity: sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==} + engines: {node: '>=0.6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + update-browserslist-db@1.2.0: + resolution: {integrity: sha512-Dn+NlSF/7+0lVSEZ57SYQg6/E44arLzsVOGgrElBn/BlG1B8WKdbLppOocFrXwRNTkNlgdGNaBgH1o0lggDPiw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@drizzle-team/brocli@0.10.2': {} + + '@emnapi/runtime@1.7.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.13.0 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.4': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/react-dom@2.1.6(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@floating-ui/dom': 1.7.4 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + + '@floating-ui/utils@0.2.10': {} + + '@img/colour@1.0.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.7.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@next/env@15.5.7': {} + + '@next/swc-darwin-arm64@15.5.7': + optional: true + + '@next/swc-darwin-x64@15.5.7': + optional: true + + '@next/swc-linux-arm64-gnu@15.5.7': + optional: true + + '@next/swc-linux-arm64-musl@15.5.7': + optional: true + + '@next/swc-linux-x64-gnu@15.5.7': + optional: true + + '@next/swc-linux-x64-musl@15.5.7': + optional: true + + '@next/swc-win32-arm64-msvc@15.5.7': + optional: true + + '@next/swc-win32-x64-msvc@15.5.7': + optional: true + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-accessible-icon@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-aspect-ratio@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-avatar@1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.4)(react@19.1.2)': + dependencies: + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-context@1.1.2(@types/react@19.1.4)(react@19.1.2)': + dependencies: + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + aria-hidden: 1.2.6 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + react-remove-scroll: 2.7.2(@types/react@19.1.4)(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-direction@1.1.1(@types/react@19.1.4)(react@19.1.2)': + dependencies: + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.1.4)(react@19.1.2)': + dependencies: + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-form@0.1.8(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-id@1.1.1(@types/react@19.1.4)(react@19.1.2)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-label@2.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + aria-hidden: 1.2.6 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + react-remove-scroll: 2.7.2(@types/react@19.1.4)(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-menubar@1.1.16(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-one-time-password-field@0.1.8(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-password-toggle-field@0.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + aria-hidden: 1.2.6 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + react-remove-scroll: 2.7.2(@types/react@19.1.4)(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@floating-ui/react-dom': 2.1.6(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/rect': 1.1.1 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-progress@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-select@2.2.6(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + aria-hidden: 1.2.6 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + react-remove-scroll: 2.7.2(@types/react@19.1.4)(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-separator@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-slider@1.3.6(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-slot@1.2.3(@types/react@19.1.4)(react@19.1.2)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-toast@1.2.15(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-toolbar@1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.4)(react@19.1.2)': + dependencies: + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.1.4)(react@19.1.2)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.1.4)(react@19.1.2)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.1.4)(react@19.1.2)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.1.4)(react@19.1.2)': + dependencies: + react: 19.1.2 + use-sync-external-store: 1.6.0(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.4)(react@19.1.2)': + dependencies: + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.1.4)(react@19.1.2)': + dependencies: + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.1.4)(react@19.1.2)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.1.4)(react@19.1.2)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + react: 19.1.2 + optionalDependencies: + '@types/react': 19.1.4 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + '@radix-ui/rect@1.1.1': {} + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.1.7': + dependencies: + '@ampproject/remapping': 2.3.0 + enhanced-resolve: 5.18.3 + jiti: 2.6.1 + lightningcss: 1.30.1 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.7 + + '@tailwindcss/oxide-android-arm64@4.1.7': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.7': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.7': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.7': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.7': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.7': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.7': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.7': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.7': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.7': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.7': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.7': + optional: true + + '@tailwindcss/oxide@4.1.7': + dependencies: + detect-libc: 2.1.2 + tar: 7.5.2 + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.7 + '@tailwindcss/oxide-darwin-arm64': 4.1.7 + '@tailwindcss/oxide-darwin-x64': 4.1.7 + '@tailwindcss/oxide-freebsd-x64': 4.1.7 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.7 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.7 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.7 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.7 + '@tailwindcss/oxide-linux-x64-musl': 4.1.7 + '@tailwindcss/oxide-wasm32-wasi': 4.1.7 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.7 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.7 + + '@tailwindcss/postcss@4.1.7': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.1.7 + '@tailwindcss/oxide': 4.1.7 + postcss: 8.5.6 + tailwindcss: 4.1.7 + + '@types/node@22.19.1': + dependencies: + undici-types: 6.21.0 + + '@types/react-dom@19.1.5(@types/react@19.1.4)': + dependencies: + '@types/react': 19.1.4 + + '@types/react@19.1.4': + dependencies: + csstype: 3.2.3 + + arg@4.1.0: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + autoprefixer@10.4.22(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + caniuse-lite: 1.0.30001759 + fraction.js: 5.3.4 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + baseline-browser-mapping@2.9.0: {} + + bcryptjs@3.0.3: {} + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.0 + caniuse-lite: 1.0.30001759 + electron-to-chromium: 1.5.263 + node-releases: 2.0.27 + update-browserslist-db: 1.2.0(browserslist@4.28.1) + + buffer-from@1.1.2: {} + + bytes@3.1.0: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + caniuse-lite@1.0.30001759: {} + + chownr@3.0.0: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + client-only@0.0.1: {} + + clsx@2.1.1: {} + + content-type@1.0.4: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + depd@1.1.2: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + detect-node-es@1.1.0: {} + + dotenv@16.6.1: {} + + drizzle-kit@0.31.7: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.25.12 + esbuild-register: 3.6.0(esbuild@0.25.12) + transitivePeerDependencies: + - supports-color + + drizzle-orm@0.43.1(postgres@3.4.7): + optionalDependencies: + postgres: 3.4.7 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.263: {} + + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + esbuild-register@3.6.0(esbuild@0.25.12): + dependencies: + debug: 4.4.3 + esbuild: 0.25.12 + transitivePeerDependencies: + - supports-color + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + escalade@3.2.0: {} + + fraction.js@5.3.4: {} + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-nonce@1.0.1: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + http-errors@1.7.3: + dependencies: + depd: 1.1.2 + inherits: 2.0.4 + setprototypeof: 1.1.1 + statuses: 1.5.0 + toidentifier: 1.0.0 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + + jiti@2.6.1: {} + + jose@6.1.3: {} + + lightningcss-darwin-arm64@1.30.1: + optional: true + + lightningcss-darwin-x64@1.30.1: + optional: true + + lightningcss-freebsd-x64@1.30.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.1: + optional: true + + lightningcss-linux-arm64-gnu@1.30.1: + optional: true + + lightningcss-linux-arm64-musl@1.30.1: + optional: true + + lightningcss-linux-x64-gnu@1.30.1: + optional: true + + lightningcss-linux-x64-musl@1.30.1: + optional: true + + lightningcss-win32-arm64-msvc@1.30.1: + optional: true + + lightningcss-win32-x64-msvc@1.30.1: + optional: true + + lightningcss@1.30.1: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + lightningcss-win32-arm64-msvc: 1.30.1 + lightningcss-win32-x64-msvc: 1.30.1 + + lucide-react@0.511.0(react@19.1.2): + dependencies: + react: 19.1.2 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + micro@10.0.1: + dependencies: + arg: 4.1.0 + content-type: 1.0.4 + raw-body: 2.4.1 + + minipass@7.1.2: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.2 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + next@15.5.7(react-dom@19.1.2(react@19.1.2))(react@19.1.2): + dependencies: + '@next/env': 15.5.7 + '@swc/helpers': 0.5.15 + caniuse-lite: 1.0.30001759 + postcss: 8.4.31 + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + styled-jsx: 5.1.6(react@19.1.2) + optionalDependencies: + '@next/swc-darwin-arm64': 15.5.7 + '@next/swc-darwin-x64': 15.5.7 + '@next/swc-linux-arm64-gnu': 15.5.7 + '@next/swc-linux-arm64-musl': 15.5.7 + '@next/swc-linux-x64-gnu': 15.5.7 + '@next/swc-linux-x64-musl': 15.5.7 + '@next/swc-win32-arm64-msvc': 15.5.7 + '@next/swc-win32-x64-msvc': 15.5.7 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + node-releases@2.0.27: {} + + normalize-range@0.1.2: {} + + object-inspect@1.13.4: {} + + picocolors@1.1.1: {} + + postcss-value-parser@4.2.0: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres@3.4.7: {} + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + radix-ui@1.4.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2): + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-accessible-icon': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-alert-dialog': 1.1.15(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-aspect-ratio': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-avatar': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-checkbox': 1.3.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-context-menu': 2.2.16(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-form': 0.1.8(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-hover-card': 1.1.15(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-menubar': 1.1.16(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-navigation-menu': 1.2.14(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-one-time-password-field': 0.1.8(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-password-toggle-field': 0.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-progress': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-radio-group': 1.3.8(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-select': 2.2.6(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slider': 1.3.6(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-switch': 1.2.6(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-toast': 1.2.15(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-toolbar': 1.1.11(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.4)(react@19.1.2) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + react: 19.1.2 + react-dom: 19.1.2(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + + raw-body@2.4.1: + dependencies: + bytes: 3.1.0 + http-errors: 1.7.3 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + react-dom@19.1.2(react@19.1.2): + dependencies: + react: 19.1.2 + scheduler: 0.26.0 + + react-remove-scroll-bar@2.3.8(@types/react@19.1.4)(react@19.1.2): + dependencies: + react: 19.1.2 + react-style-singleton: 2.2.3(@types/react@19.1.4)(react@19.1.2) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.4 + + react-remove-scroll@2.7.2(@types/react@19.1.4)(react@19.1.2): + dependencies: + react: 19.1.2 + react-remove-scroll-bar: 2.3.8(@types/react@19.1.4)(react@19.1.2) + react-style-singleton: 2.2.3(@types/react@19.1.4)(react@19.1.2) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.1.4)(react@19.1.2) + use-sidecar: 1.1.3(@types/react@19.1.4)(react@19.1.2) + optionalDependencies: + '@types/react': 19.1.4 + + react-style-singleton@2.2.3(@types/react@19.1.4)(react@19.1.2): + dependencies: + get-nonce: 1.0.1 + react: 19.1.2 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.4 + + react@19.1.2: {} + + resolve-pkg-maps@1.0.0: {} + + safer-buffer@2.1.2: {} + + scheduler@0.26.0: {} + + semver@7.7.3: + optional: true + + server-only@0.0.1: {} + + setprototypeof@1.1.1: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + optional: true + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + statuses@1.5.0: {} + + stripe@18.5.0(@types/node@22.19.1): + dependencies: + qs: 6.14.0 + optionalDependencies: + '@types/node': 22.19.1 + + styled-jsx@5.1.6(react@19.1.2): + dependencies: + client-only: 0.0.1 + react: 19.1.2 + + swr@2.3.7(react@19.1.2): + dependencies: + dequal: 2.0.3 + react: 19.1.2 + use-sync-external-store: 1.6.0(react@19.1.2) + + tailwind-merge@3.4.0: {} + + tailwindcss@4.1.7: {} + + tapable@2.3.0: {} + + tar@7.5.2: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.1.0 + yallist: 5.0.0 + + toidentifier@1.0.0: {} + + tslib@2.8.1: {} + + tw-animate-css@1.4.0: {} + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + unpipe@1.0.0: {} + + update-browserslist-db@1.2.0(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-callback-ref@1.3.3(@types/react@19.1.4)(react@19.1.2): + dependencies: + react: 19.1.2 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.4 + + use-sidecar@1.1.3(@types/react@19.1.4)(react@19.1.2): + dependencies: + detect-node-es: 1.1.0 + react: 19.1.2 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.4 + + use-sync-external-store@1.6.0(react@19.1.2): + dependencies: + react: 19.1.2 + + yallist@5.0.0: {} + + zod@3.25.76: {} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/postcss.config.mjs b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/postcss.config.mjs new file mode 100644 index 000000000..a34a3d560 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + }, +}; diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/public/favicon.ico b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/public/favicon.ico new file mode 100644 index 000000000..718d6fea4 Binary files /dev/null and b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/public/favicon.ico differ diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/styles/globals.css b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/styles/globals.css new file mode 100644 index 000000000..794a380a2 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/styles/globals.css @@ -0,0 +1,275 @@ +@import "tailwindcss"; +/* + ---break--- +*/ +@custom-variant dark (&:is(.dark *)); + +@import "tw-animate-css"; + +@variant dark (&:is(.dark *)); + +@theme { + --color-background: hsl(var(--background)); + --color-foreground: hsl(var(--foreground)); + + --color-card: hsl(var(--card)); + --color-card-foreground: hsl(var(--card-foreground)); + + --color-popover: hsl(var(--popover)); + --color-popover-foreground: hsl(var(--popover-foreground)); + + --color-primary: hsl(var(--primary)); + --color-primary-foreground: hsl(var(--primary-foreground)); + + --color-secondary: hsl(var(--secondary)); + --color-secondary-foreground: hsl(var(--secondary-foreground)); + + --color-muted: hsl(var(--muted)); + --color-muted-foreground: hsl(var(--muted-foreground)); + + --color-accent: hsl(var(--accent)); + --color-accent-foreground: hsl(var(--accent-foreground)); + + --color-destructive: hsl(var(--destructive)); + --color-destructive-foreground: hsl(var(--destructive-foreground)); + + --color-border: hsl(var(--border)); + --color-input: hsl(var(--input)); + --color-ring: hsl(var(--ring)); + + --color-chart-1: hsl(var(--chart-1)); + --color-chart-2: hsl(var(--chart-2)); + --color-chart-3: hsl(var(--chart-3)); + --color-chart-4: hsl(var(--chart-4)); + --color-chart-5: hsl(var(--chart-5)); + + --color-sidebar: hsl(var(--sidebar-background)); + --color-sidebar-foreground: hsl(var(--sidebar-foreground)); + --color-sidebar-primary: hsl(var(--sidebar-primary)); + --color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground)); + --color-sidebar-accent: hsl(var(--sidebar-accent)); + --color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground)); + --color-sidebar-border: hsl(var(--sidebar-border)); + --color-sidebar-ring: hsl(var(--sidebar-ring)); + + --radius-lg: var(--radius); + --radius-md: calc(var(--radius) - 2px); + --radius-sm: calc(var(--radius) - 4px); +} + +/* + The default border color has changed to `currentColor` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. +*/ +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentColor); + } +} + +@layer utilities { + body { + font-family: "Manrope", Arial, Helvetica, sans-serif; + } +} + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} + +/* + ---break--- +*/ + +:root { + --background: hsl(0 0% 100%); + --foreground: hsl(240 10% 3.9%); + --card: hsl(0 0% 100%); + --card-foreground: hsl(240 10% 3.9%); + --popover: hsl(0 0% 100%); + --popover-foreground: hsl(240 10% 3.9%); + --primary: hsl(240 5.9% 10%); + --primary-foreground: hsl(0 0% 98%); + --secondary: hsl(240 4.8% 95.9%); + --secondary-foreground: hsl(240 5.9% 10%); + --muted: hsl(240 4.8% 95.9%); + --muted-foreground: hsl(240 3.8% 46.1%); + --accent: hsl(240 4.8% 95.9%); + --accent-foreground: hsl(240 5.9% 10%); + --destructive: hsl(0 84.2% 60.2%); + --destructive-foreground: hsl(0 0% 98%); + --border: hsl(240 5.9% 90%); + --input: hsl(240 5.9% 90%); + --ring: hsl(240 10% 3.9%); + --chart-1: hsl(12 76% 61%); + --chart-2: hsl(173 58% 39%); + --chart-3: hsl(197 37% 24%); + --chart-4: hsl(43 74% 66%); + --chart-5: hsl(27 87% 67%); + --radius: 0.6rem; +} + +/* + ---break--- +*/ + +.dark { + --background: hsl(240 10% 3.9%); + --foreground: hsl(0 0% 98%); + --card: hsl(240 10% 3.9%); + --card-foreground: hsl(0 0% 98%); + --popover: hsl(240 10% 3.9%); + --popover-foreground: hsl(0 0% 98%); + --primary: hsl(0 0% 98%); + --primary-foreground: hsl(240 5.9% 10%); + --secondary: hsl(240 3.7% 15.9%); + --secondary-foreground: hsl(0 0% 98%); + --muted: hsl(240 3.7% 15.9%); + --muted-foreground: hsl(240 5% 64.9%); + --accent: hsl(240 3.7% 15.9%); + --accent-foreground: hsl(0 0% 98%); + --destructive: hsl(0 62.8% 30.6%); + --destructive-foreground: hsl(0 0% 98%); + --border: hsl(240 3.7% 15.9%); + --input: hsl(240 3.7% 15.9%); + --ring: hsl(240 4.9% 83.9%); + --chart-1: hsl(220 70% 50%); + --chart-2: hsl(160 60% 45%); + --chart-3: hsl(30 80% 55%); + --chart-4: hsl(280 65% 60%); + --chart-5: hsl(340 75% 55%); +} + +/* + ---break--- +*/ + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); +} + +/* + ---break--- +*/ + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/tsconfig.json b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/tsconfig.json new file mode 100644 index 000000000..f969d125d --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-saas/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "baseUrl": ".", + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-todo/COMPARISON.md b/apps/error-tracking/no-posthog-next-js/15-pages-router-todo/COMPARISON.md new file mode 100644 index 000000000..86b979359 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-todo/COMPARISON.md @@ -0,0 +1,253 @@ +# App Router vs Pages Router: Side-by-Side Comparison + +This document highlights the specific code differences between the App Router and Pages Router implementations of the same todo application. + +## 1. Entry Points and Layout + +### App Router ([app/layout.tsx](../15-app-router-todo/app/layout.tsx)) +```tsx +import type { Metadata } from 'next'; +import './globals.css'; + +export const metadata: Metadata = { + title: 'Todo App - Next.js 15', + description: 'A full-stack todo application built with Next.js 15', +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} +``` + +### Pages Router ([pages/_app.tsx](pages/_app.tsx) + [pages/_document.tsx](pages/_document.tsx)) +```tsx +// pages/_app.tsx +import type { AppProps } from 'next/app'; +import '@/styles/globals.css'; + +export default function App({ Component, pageProps }: AppProps) { + return ; +} + +// pages/_document.tsx +import { Html, Head, Main, NextScript } from 'next/document'; + +export default function Document() { + return ( + + + +
+ + + + ); +} +``` + +**Key Differences:** +- App Router: Single `layout.tsx` with metadata export +- Pages Router: Separate `_app.tsx` and `_document.tsx` files + +## 2. Page Component + +### App Router ([app/page.tsx](../15-app-router-todo/app/page.tsx)) +```tsx +import { TodoList } from '@/components/todos/todo-list'; + +export default function HomePage() { + return ; +} +``` + +### Pages Router ([pages/index.tsx](pages/index.tsx)) +```tsx +import Head from 'next/head'; +import { TodoList } from '@/components/todos/todo-list'; + +export default function HomePage() { + return ( + <> + + Todo App - Next.js 15 + + + + + ); +} +``` + +**Key Differences:** +- Pages Router: Uses `next/head` for metadata instead of `metadata` export + +## 3. API Routes - Collection Endpoint + +### App Router ([app/api/todos/route.ts](../15-app-router-todo/app/api/todos/route.ts)) +```tsx +import { NextRequest, NextResponse } from 'next/server'; +import { getTodos, createTodo } from '@/lib/data'; + +// Named export for GET method +export async function GET() { + try { + const allTodos = getTodos(); + return NextResponse.json(allTodos); + } catch (error) { + return NextResponse.json( + { error: 'Failed to fetch todos' }, + { status: 500 } + ); + } +} + +// Named export for POST method +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + // ... validation and creation + return NextResponse.json(newTodo, { status: 201 }); + } catch (error) { + // ... error handling + } +} +``` + +### Pages Router ([pages/api/todos/index.ts](pages/api/todos/index.ts)) +```tsx +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getTodos, createTodo } from '@/lib/data'; + +// Single default export handler +export default function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === 'GET') { + try { + const allTodos = getTodos(); + return res.status(200).json(allTodos); + } catch (error) { + return res.status(500).json({ error: 'Failed to fetch todos' }); + } + } + + if (req.method === 'POST') { + try { + const validatedData = todoSchema.parse(req.body); + // ... creation logic + return res.status(201).json(newTodo); + } catch (error) { + // ... error handling + } + } + + return res.status(405).json({ error: 'Method not allowed' }); +} +``` + +**Key Differences:** +- App Router: Separate named exports (GET, POST) using `NextRequest`/`NextResponse` +- Pages Router: Single handler with method checking using `NextApiRequest`/`NextApiResponse` + +## 4. API Routes - Dynamic Routes + +### App Router ([app/api/todos/[id]/route.ts](../15-app-router-todo/app/api/todos/[id]/route.ts)) +```tsx +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params; // params is a Promise in Next.js 15 + const todoId = parseInt(id); + // ... +} +``` + +### Pages Router ([pages/api/todos/[id].ts](pages/api/todos/[id].ts)) +```tsx +export default function handler(req: NextApiRequest, res: NextApiResponse) { + const { id } = req.query; // query params available directly + const todoId = parseInt(id as string); + + if (req.method === 'GET') { + // ... + } + + if (req.method === 'PATCH') { + // ... + } + + if (req.method === 'DELETE') { + // ... + } + + return res.status(405).json({ error: 'Method not allowed' }); +} +``` + +**Key Differences:** +- App Router: + - Params passed as second argument + - Params is a Promise (must await in Next.js 15) + - Located in `[id]/route.ts` +- Pages Router: + - Params available via `req.query` + - Synchronous access + - Located in `[id].ts` + +## 5. CSS Location + +### App Router +- Located at `app/globals.css` +- Imported in `app/layout.tsx` + +### Pages Router +- Located at `styles/globals.css` +- Imported in `pages/_app.tsx` + +## 6. Configuration + +### components.json +```json +// App Router +{ + "rsc": true, + "tailwind": { + "css": "app/globals.css" + } +} + +// Pages Router +{ + "rsc": false, + "tailwind": { + "css": "styles/globals.css" + } +} +``` + +**Key Differences:** +- App Router: RSC (React Server Components) enabled +- Pages Router: RSC disabled (client components by default) + +## Summary + +Both implementations are **functionally identical** and use the same: +- Components (`components/`) +- Business logic (`lib/`) +- Styling approach (Tailwind CSS) +- Validation (Zod) +- Type system (TypeScript) + +The only differences are in: +1. **File structure**: `app/` vs `pages/` +2. **Metadata handling**: `metadata` export vs `next/head` +3. **API route syntax**: Named exports vs method checking +4. **Params handling**: Async params vs sync query object +5. **Response types**: `NextResponse` vs direct `res` object diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-todo/README.md b/apps/error-tracking/no-posthog-next-js/15-pages-router-todo/README.md new file mode 100644 index 000000000..08d683890 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-todo/README.md @@ -0,0 +1,137 @@ +# Next.js 15 Todo App (Pages Router) + +A simple, full-stack todo application built with Next.js 15, utilizing the Pages Router with server-side API routes and in-memory storage for managing todo items. + +## Features + +- **Full CRUD Operations**: Create, Read, Update, and Delete todos +- **Server-Side API Routes**: RESTful API endpoints using Next.js 15 Pages Router API routes +- **In-Memory Storage**: Simple data persistence (resets on server restart) +- **Modern UI**: Clean interface with Tailwind CSS and custom components +- **Type Safety**: Full TypeScript support with Zod validation + +## Tech Stack + +- **Next.js 15.3** - React framework with Pages Router +- **React 19** - UI library +- **TypeScript** - Type safety +- **Tailwind CSS 4** - Styling +- **Zod** - Schema validation +- **Lucide React** - Icons + +## Getting Started + +### Prerequisites + +- Node.js 18+ and pnpm + +### Installation + +1. Install dependencies: +```bash +pnpm install +``` + +2. Start the development server: +```bash +pnpm dev +``` + +Open [http://localhost:3000](http://localhost:3000) to see the app. + +## API Routes + +### GET /api/todos +Get all todos + +### POST /api/todos +Create a new todo +```json +{ + "title": "Learn Next.js", + "description": "Study Next.js 15 features", + "completed": false +} +``` + +### GET /api/todos/[id] +Get a specific todo by ID + +### PATCH /api/todos/[id] +Update a todo +```json +{ + "completed": true +} +``` + +### DELETE /api/todos/[id] +Delete a todo + +## Project Structure + +``` +apps/next-js/15-pages-router-todo/ +├── pages/ +│ ├── _app.tsx # Custom App component +│ ├── _document.tsx # Custom Document component +│ ├── index.tsx # Home page +│ └── api/ +│ └── todos/ +│ ├── index.ts # GET, POST /api/todos +│ └── [id].ts # GET, PATCH, DELETE /api/todos/[id] +├── components/ +│ ├── todos/ +│ │ ├── todo-list.tsx # Main todo list component +│ │ ├── todo-item.tsx # Individual todo item +│ │ └── todo-form.tsx # Form for adding todos +│ └── ui/ # Reusable UI components +├── lib/ +│ ├── data.ts # In-memory data store +│ └── utils.ts +├── styles/ +│ └── globals.css # Global styles +└── package.json +``` + +## Pages Router vs App Router + +This project is a Pages Router implementation of the App Router todo application. The key differences include: + +### Routing +- **Pages Router**: Uses `pages/` directory for file-based routing +- **App Router**: Uses `app/` directory for file-based routing + +### Page Structure +- **Pages Router**: + - Uses `_app.tsx` for global app wrapper + - Uses `_document.tsx` for HTML document structure + - Metadata managed via `next/head` component +- **App Router**: + - Uses `layout.tsx` for layouts + - Metadata managed via `metadata` export + +### API Routes +- **Pages Router**: + - Located in `pages/api/` + - Uses `NextApiRequest` and `NextApiResponse` + - Single handler function that checks `req.method` +- **App Router**: + - Located in `app/api/*/route.ts` + - Uses `NextRequest` and `NextResponse` + - Named exports for each HTTP method (GET, POST, etc.) + +### Dynamic Routes +- **Pages Router**: Uses `[id].ts` with `req.query.id` (string) +- **App Router**: Uses `[id]/route.ts` with async `params` (Promise) + +## Notes + +- Data is stored in memory and will be reset when the development server restarts +- The app comes pre-seeded with 3 sample todos +- Perfect for learning Next.js 15 Pages Router without database complexity +- Functionally identical to the App Router version, just using different routing paradigms + +## License + +MIT diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-todo/components.json b/apps/error-tracking/no-posthog-next-js/15-pages-router-todo/components.json new file mode 100644 index 000000000..2347ee76b --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-todo/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "styles/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-todo/components/todos/todo-form.tsx b/apps/error-tracking/no-posthog-next-js/15-pages-router-todo/components/todos/todo-form.tsx new file mode 100644 index 000000000..2d9362784 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-todo/components/todos/todo-form.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; + +interface TodoFormProps { + onAdd: (title: string, description: string) => void; +} + +export function TodoForm({ onAdd }: TodoFormProps) { + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (title.trim()) { + onAdd(title, description); + setTitle(''); + setDescription(''); + } + }; + + return ( +
+
+ setTitle(e.target.value)} + className="w-full" + /> +
+
+ setDescription(e.target.value)} + className="w-full" + /> +
+ +
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-todo/components/todos/todo-item.tsx b/apps/error-tracking/no-posthog-next-js/15-pages-router-todo/components/todos/todo-item.tsx new file mode 100644 index 000000000..49c20fb89 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-todo/components/todos/todo-item.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { Todo } from '@/lib/data'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Trash2 } from 'lucide-react'; + +interface TodoItemProps { + todo: Todo; + onToggle: (id: number, completed: boolean) => void; + onDelete: (id: number) => void; +} + +export function TodoItem({ todo, onToggle, onDelete }: TodoItemProps) { + return ( + + +
+
+ onToggle(todo.id, e.target.checked)} + className="mt-1" + /> +
+ + {todo.title} + + {todo.description && ( + + {todo.description} + + )} +
+
+ +
+
+
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-todo/components/todos/todo-list.tsx b/apps/error-tracking/no-posthog-next-js/15-pages-router-todo/components/todos/todo-list.tsx new file mode 100644 index 000000000..9bad4230f --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-todo/components/todos/todo-list.tsx @@ -0,0 +1,165 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { Todo } from '@/lib/data'; +import { TodoForm } from './todo-form'; +import { TodoItem } from './todo-item'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; + +export function TodoList() { + const [todos, setTodos] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetchTodos(); + }, []); + + const fetchTodos = async () => { + try { + const response = await fetch('/api/todos'); + if (response.ok) { + const data = await response.json(); + setTodos(data); + } + } catch (error) { + console.error('Failed to fetch todos:', error); + } finally { + setLoading(false); + } + }; + + const handleAddTodo = async (title: string, description: string) => { + try { + const response = await fetch('/api/todos', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ title, description }), + }); + + if (response.ok) { + const newTodo = await response.json(); + setTodos([...todos, newTodo]); + } + } catch (error) { + console.error('Failed to add todo:', error); + } + }; + + const handleToggleTodo = async (id: number, completed: boolean) => { + try { + const response = await fetch(`/api/todos/${id}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ completed }), + }); + + if (response.ok) { + const updatedTodo = await response.json(); + setTodos(todos.map((todo) => (todo.id === id ? updatedTodo : todo))); + } + } catch (error) { + console.error('Failed to update todo:', error); + } + }; + + const handleDeleteTodo = async (id: number) => { + try { + const response = await fetch(`/api/todos/${id}`, { + method: 'DELETE', + }); + + if (response.ok) { + setTodos(todos.filter((todo) => todo.id !== id)); + } + } catch (error) { + console.error('Failed to delete todo:', error); + } + }; + + if (loading) { + return ( +
+

Loading...

+
+ ); + } + + const activeTodos = todos.filter((todo) => !todo.completed); + const completedTodos = todos.filter((todo) => todo.completed); + + return ( +
+
+
+

Todo App

+ + About + +
+

+ Manage your tasks with Next.js 15 server-side API routes +

+
+ + + + Add New Todo + Create a new task to keep track of + + + + + + +
+
+

+ Active Tasks ({activeTodos.length}) +

+ {activeTodos.length === 0 ? ( +

+ No active tasks. Add one above! +

+ ) : ( +
+ {activeTodos.map((todo) => ( + + ))} +
+ )} +
+ + {completedTodos.length > 0 && ( +
+

+ Completed ({completedTodos.length}) +

+
+ {completedTodos.map((todo) => ( + + ))} +
+
+ )} +
+
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-js/15-pages-router-todo/components/ui/button.tsx b/apps/error-tracking/no-posthog-next-js/15-pages-router-todo/components/ui/button.tsx new file mode 100644 index 000000000..5210bd6e9 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-js/15-pages-router-todo/components/ui/button.tsx @@ -0,0 +1,54 @@ +import * as React from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { cn } from '@/lib/utils'; + +const buttonVariants = cva( + 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', + { + variants: { + variant: { + default: + 'bg-primary text-primary-foreground shadow hover:bg-primary/90', + destructive: + 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', + outline: + 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', + secondary: + 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-9 px-4 py-2', + sm: 'h-8 rounded-md px-3 text-xs', + lg: 'h-10 rounded-md px-8', + icon: 'h-9 w-9', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, ...props }, ref) => { + return ( +