diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f11cb6d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,104 @@ +name: CI + +on: + push: + branches: [main] + paths: + - "community-reputation/**" + - ".github/workflows/ci.yml" + pull_request: + paths: + - "community-reputation/**" + - ".github/workflows/ci.yml" + +defaults: + run: + working-directory: community-reputation + +jobs: + typecheck: + name: TypeScript + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: community-reputation/package-lock.json + + - run: npm ci + + - name: Generate Prisma client + run: npx prisma generate + + - run: npx tsc --noEmit + + unit-tests: + name: Unit tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: community-reputation/package-lock.json + + - run: npm ci + + - name: Generate Prisma client + run: npx prisma generate + + - run: npm run test:unit + + integration-tests: + name: Integration tests + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: scibase_community_test + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: community-reputation/package-lock.json + + - run: npm ci + + - name: Generate Prisma client + run: npx prisma generate + + - name: Push schema to test database + env: + DATABASE_URL: postgresql://postgres:password@localhost:5432/scibase_community_test + # db push is used here because the repo has no migration files yet. + # Once `prisma migrate dev --name init` has been run locally and the + # migrations/ folder is committed, replace this with: + # npx prisma migrate deploy + run: npx prisma db push + + - name: Run integration tests + env: + DATABASE_URL: postgresql://postgres:password@localhost:5432/scibase_community_test + # --fileParallelism=false is baked into the npm script; it prevents + # concurrent TRUNCATE calls across test files from deadlocking each other. + run: npm run test:integration diff --git a/community-reputation/.dockerignore b/community-reputation/.dockerignore new file mode 100644 index 0000000..6e2348d --- /dev/null +++ b/community-reputation/.dockerignore @@ -0,0 +1,5 @@ +node_modules +.next +.env +*.log +tsconfig.tsbuildinfo diff --git a/community-reputation/.env.example b/community-reputation/.env.example new file mode 100644 index 0000000..1d386f1 --- /dev/null +++ b/community-reputation/.env.example @@ -0,0 +1,12 @@ +DATABASE_URL="postgresql://postgres:password@localhost:5432/scibase_community" +DATABASE_URL_TEST="postgresql://postgres:password@localhost:5432/scibase_community_test" + +NEXTAUTH_URL="http://localhost:3000" +NEXTAUTH_SECRET="replace-with-a-random-secret-openssl-rand-base64-32" + +# GitHub OAuth App — create one at github.com/settings/developers +GITHUB_ID="your-github-oauth-app-client-id" +GITHUB_SECRET="your-github-oauth-app-client-secret" + +# Secret header value for the cron endpoint +CRON_SECRET="replace-with-a-random-secret" diff --git a/community-reputation/.env.test.example b/community-reputation/.env.test.example new file mode 100644 index 0000000..daa6e98 --- /dev/null +++ b/community-reputation/.env.test.example @@ -0,0 +1 @@ +DATABASE_URL="postgresql://postgres:password@localhost:5432/scibase_community_test" diff --git a/community-reputation/.gitignore b/community-reputation/.gitignore new file mode 100644 index 0000000..fd6de24 --- /dev/null +++ b/community-reputation/.gitignore @@ -0,0 +1,26 @@ +# Dependencies +node_modules/ + +# Next.js build output +.next/ +out/ + +# Environment files — never commit real secrets +.env +.env.local +.env.test + +# Prisma generated client (regenerated on install) +# Keep migrations/ committed +prisma/generated/ + +# TypeScript incremental build cache +tsconfig.tsbuildinfo +*.tsbuildinfo + +# OS / editor noise +.DS_Store +*.swp +*.swo +.idea/ +.vscode/ diff --git a/community-reputation/Dockerfile b/community-reputation/Dockerfile new file mode 100644 index 0000000..deff5c6 --- /dev/null +++ b/community-reputation/Dockerfile @@ -0,0 +1,33 @@ +# ── Build stage ─────────────────────────────────────────────────────────────── +FROM node:20-alpine AS builder +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY prisma ./prisma +RUN npx prisma generate + +COPY . . +RUN npm run build + +# ── Runtime stage ───────────────────────────────────────────────────────────── +FROM node:20-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production + +# Copy only what's needed to run +COPY --from=builder /app/package*.json ./ +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/.next ./.next +COPY --from=builder /app/prisma ./prisma +COPY --from=builder /app/src/lib ./src/lib + +EXPOSE 3000 + +# entrypoint.sh: migrate → seed → (optional demo seed) → start +COPY docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/community-reputation/README.md b/community-reputation/README.md new file mode 100644 index 0000000..61393d8 --- /dev/null +++ b/community-reputation/README.md @@ -0,0 +1,312 @@ +# SCIBASE Community & Reputation System + +Data + API layer for Issue #15 — peer reviews, CRediT contributor credits, and transparent reputation scoring. + +## Stack + +| Layer | Choice | +|---|---| +| Framework | Next.js 14 (App Router) | +| Language | TypeScript (strict) | +| Database | PostgreSQL 16 | +| ORM | Prisma 5 | +| Auth | NextAuth.js v4 (GitHub provider — swap as needed) | +| Validation | Zod | +| Tests | Vitest | + +## Setup + +The fastest way to run the project is `dev.sh`. It handles Postgres, migrations, seeding, and the dev server in one step. The only hard requirement is Docker (for Postgres). + +```bash +# If you have Node ≥ 18 installed: +./dev.sh --demo # seed sample data + start hot-reload dev server + +# If you only have Docker (no Node required on host): +./dev.sh --demo --docker # builds the image, runs everything in containers +``` + +The `--demo` flag seeds four researchers (Alice Chen, Bob Mehta, Claire Dupont, Diana Osei) with projects, reviews, endorsements, and reputation snapshots so you can explore the UI immediately. + +### Manual setup (optional) + +```bash +# 1. Start Postgres +docker compose up -d postgres + +# 2. Install dependencies +npm install + +# 3. Configure env +cp .env.example .env +# Edit .env — fill in NEXTAUTH_SECRET, GITHUB_ID, GITHUB_SECRET, CRON_SECRET + +# 4. Run migrations & generate Prisma client +npm run db:migrate + +# 5. Seed review templates and badges +npm run db:seed + +# 6. Start dev server +npm run dev +``` + +## Running tests + +```bash +# Unit tests (no database required) +npm run test:unit + +# Integration tests (requires Postgres) +cp .env.test.example .env.test +npm run db:migrate:test # migrate the test database +dotenv -e .env.test -- npm run test:integration +``` + +## Project layout + +``` +community-reputation/ +├── prisma/ +│ ├── schema.prisma # Full data model +│ └── seed.ts # Seeds templates + badges +├── src/ +│ ├── lib/ +│ │ ├── credit.ts # CRediT taxonomy enum + labels +│ │ ├── reputation.ts # Pure scoring function (log1p smoothed) +│ │ ├── visibility.ts # serializeReview() — anonymity enforcement +│ │ ├── badges.ts # evaluateBadges() — threshold checks +│ │ ├── review-templates.ts # Discipline-specific template definitions +│ │ ├── auth.ts # NextAuth config + requireAuth/getAuth helpers +│ │ ├── prisma.ts # Singleton Prisma client +│ │ └── api.ts # Shared error handler +│ ├── services/ # Business logic + DB queries (testable without HTTP) +│ │ ├── review.service.ts +│ │ ├── comment.service.ts +│ │ ├── contribution.service.ts +│ │ ├── endorsement.service.ts +│ │ └── reputation.service.ts +│ └── app/api/ # Thin route handlers (parse → service → respond) +│ ├── auth/[...nextauth]/ +│ ├── reviews/ +│ ├── comments/ +│ ├── contributions/ +│ ├── endorsements/ +│ ├── reputation/[userId]/ +│ ├── reputation/[userId]/history/ +│ ├── users/[userId]/badges/ +│ ├── leaderboards/ +│ └── cron/reputation-snapshots/ +└── tests/ + ├── unit/ # reputation, visibility, badges — no DB needed + └── integration/ # full flows against a real test database +``` + +## API Reference + +All write endpoints require a valid NextAuth session cookie. Read endpoints are public but apply visibility enforcement. + +### Reviews + +#### `POST /api/reviews` +Create a new review (starts as DRAFT). + +```json +{ + "projectId": "cuid", + "templateId": "cuid (optional)", + "clarity": 1–5, + "rigor": 1–5, + "novelty": 1–5, + "reproducibility": 1–5, + "content": "string (required)", + "visibility": "PUBLIC | SEMI_PRIVATE | ANONYMOUS | DOUBLE_BLIND" +} +``` + +#### `GET /api/reviews?projectId=&reviewerId=&status=` +List reviews. Defaults to `status=PUBLISHED`. Reviewer identity is masked according to visibility rules. + +#### `GET /api/reviews/:id` +Fetch a single review. Applies visibility serialisation. + +#### `PATCH /api/reviews/:id` +Update a review (reviewer only). Supports any subset of: `clarity`, `rigor`, `novelty`, `reproducibility`, `content`, `visibility`, `status`. + +--- + +### Comments + +#### `POST /api/comments` +```json +{ + "body": "string", + "targetType": "DOCUMENT | DATASET | CODE_BLOCK | NOTEBOOK_CELL | REVIEW | PROJECT", + "targetId": "string", + "anchor": { "start": 0, "end": 10 }, + "parentId": "cuid (optional, for replies)" +} +``` + +#### `GET /api/comments?targetType=REVIEW&targetId=:id` +List top-level comments with nested replies. + +--- + +### Contributions (CRediT) + +#### `POST /api/contributions` +Log a contributor credit against a project. + +```json +{ + "projectId": "cuid", + "role": "CONCEPTUALIZATION | DATA_CURATION | FORMAL_ANALYSIS | ...", + "description": "string (optional)", + "occurredAt": "ISO date (optional, for backfilling)" +} +``` + +Full CRediT taxonomy: `CONCEPTUALIZATION`, `DATA_CURATION`, `FORMAL_ANALYSIS`, `FUNDING_ACQUISITION`, `INVESTIGATION`, `METHODOLOGY`, `PROJECT_ADMINISTRATION`, `RESOURCES`, `SOFTWARE`, `SUPERVISION`, `VALIDATION`, `VISUALIZATION`, `WRITING_ORIGINAL_DRAFT`, `WRITING_REVIEW_EDITING`. + +#### `GET /api/contributions?userId=&projectId=` +List contributions. Filter by user, project, or both. + +#### `DELETE /api/contributions/:id` +Soft-delete a contribution (author only). Returns `204`. + +#### `GET /api/contributions/export?userId=` +Download a CRediT-aligned CSV suitable for tenure/promotion applications. Returns `text/csv` with columns: `project_title`, `project_id`, `credit_role`, `credit_role_label`, `description`, `occurred_at`. + +--- + +### Endorsements + +#### `POST /api/endorsements` +```json +{ "endorseeId": "cuid", "skill": "peer-review", "note": "optional" } +``` +Self-endorsement returns 422. Rate-limited to 10 endorsements per calendar day per endorser (returns 429 on breach). Re-endorsing after retraction is supported. + +#### `DELETE /api/endorsements` +```json +{ "endorseeId": "cuid", "skill": "peer-review" } +``` + +#### `GET /api/endorsements?endorseeId=:id` +List all active endorsements for a user. + +--- + +### Reputation + +#### `GET /api/reputation/:userId` +Returns a live score with full breakdown. Does not write a snapshot. + +```json +{ + "user": { "id": "...", "name": "...", "institution": "...", "domain": "..." }, + "total": 42.73, + "components": { + "reviewsCompleted": 8, + "endorsementsReceived": 12, + "citationsReceived": 0, + "forksReceived": 0, + "reproducibilityVerified": 2, + "bountiesCompleted": 0, + "badgesEarned": 1 + }, + "breakdown": [ + { "component": "reviewsCompleted", "rawValue": 8, "smoothed": 2.197, "weighted": 54.9 }, + ... + ] +} +``` + +#### `GET /api/reputation/:userId/history?limit=90` +Returns up to `limit` (max 365) historical `ReputationSnapshot` records ordered by `computedAt` ascending. Used for score sparklines. + +#### `GET /api/users/:userId/badges` +Returns all badges earned by a user with full badge metadata, ordered by `awardedAt` descending. + +#### `GET /api/leaderboards?domain=®ion=&institution=&limit=50` +Ranked list from the latest `ReputationSnapshot` per user. All filters are optional. + +--- + +### Cron + +#### `GET /api/cron/reputation-snapshots` +Recomputes and persists a `ReputationSnapshot` for every user. Protected by `x-cron-secret` header. Configure in Vercel Cron or any scheduler. + +--- + +## Reputation scoring formula + +``` +score = Σ weight_i × log1p(component_i) + +Weights: + reviewsCompleted 25 + endorsementsReceived 20 + citationsReceived 20 + forksReceived 10 + reproducibilityVerified 10 + bountiesCompleted 10 + badgesEarned 5 + ───────────────────────── + total 100 +``` + +`log1p` smoothing prevents power-user dominance — each additional unit of activity contributes less than the previous one. The `breakdown` field in every API response shows the per-component contribution so scores are fully auditable. + +**Live components:** `reviewsCompleted`, `endorsementsReceived`, `reproducibilityVerified` (counts VALIDATION-role contributions), `badgesEarned`. + +**Stubbed components (return 0 until wired):** +- `citationsReceived` — wire to a DOI/citation service in `gatherComponents()` in `src/services/reputation.service.ts` +- `forksReceived` — wire to your project fork counter in the same function +- `bountiesCompleted` — wire to a `BountyCompletion` table once the bounties feature is built + +## Visibility modes + +| Mode | Authors see reviewer | Outsiders see reviewer | Reviewer sees self | +|---|---|---|---| +| PUBLIC | Yes | Yes | Yes | +| SEMI_PRIVATE | Yes | No | Yes | +| ANONYMOUS | No | No | Yes | +| DOUBLE_BLIND | No | No | Yes | + +All visibility logic flows through `serializeReview()` in `src/lib/visibility.ts` — the single enforcement point for every read path. + +## Badges + +Seeded badges and their thresholds: + +| Slug | Threshold | +|---|---| +| `trusted_reviewer` | reviewsCompleted ≥ 10 | +| `open_science_champion` | endorsementsReceived ≥ 20 | +| `reproducibility_verified` | total score ≥ 50 | +| `prolific_contributor` | citationsReceived ≥ 10 | + +Add new badges via `prisma/seed.ts` — no code changes required. + +## UI pages + +Three pages are included alongside the API: + +| Route | Description | +|---|---| +| `/leaderboard` | Ranked table with domain / region / institution filters | +| `/users/:userId` | Profile — reputation breakdown, badges, endorsements by skill, CRediT contributions | +| `/projects/:projectId` | Contributor graph (CRediT), peer reviews with star ratings, inline review submission form, threaded comments per review | + +A dev-only **Demo login bar** appears at the top of every page in development. Click any of the four seed users to sign in instantly (no OAuth setup needed). Alice Chen carries the **Admin** badge and can post reviews, comments, and endorsements from the UI. + +## Known limitations / future work + +- **Citations and forks** feed into the reputation formula but are stubbed at 0. Wire `gatherComponents()` in `src/services/reputation.service.ts` to an external DOI registry and an internal project fork counter. +- **Bounty completions** are stubbed at 0. Wire to a `BountyCompletion` table once that feature is built. +- **Reproducibility badge trigger** — the `reproducibility_verified` badge threshold is currently score-based (total ≥ 50). A production system should gate it on a formal notebook re-run verification flow. +- **Pagination** — list endpoints (`/reviews`, `/comments`, `/contributions`) return all matching rows. Add cursor-based pagination before exposing to large datasets. +- **GitHub OAuth** — `GITHUB_ID` / `GITHUB_SECRET` in `.env.example` are placeholders. The dev credentials provider is a local-only bypass; production login requires real OAuth credentials. diff --git a/community-reputation/dev.sh b/community-reputation/dev.sh new file mode 100755 index 0000000..b470dbd --- /dev/null +++ b/community-reputation/dev.sh @@ -0,0 +1,222 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────────────── +# dev.sh — start the SCIBASE Community & Reputation API locally +# +# Usage: +# ./dev.sh # auto-detect mode: local if Node ≥18, Docker otherwise +# ./dev.sh --demo # also seeds sample users, projects, reviews & endorsements +# ./dev.sh --reset # wipe the DB and re-seed before starting +# ./dev.sh --docker # force Docker mode (no Node.js required on host) +# +# Docker mode: requires only Docker Desktop — builds the image and runs +# everything (Postgres + app + migrations + seed) via compose. +# Local mode: faster hot-reload dev server; requires Node ≥18 + npm. +# ───────────────────────────────────────────────────────────────────────────── +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# ── Colours ─────────────────────────────────────────────────────────────────── +GRN='\033[0;32m'; YLW='\033[1;33m'; BLU='\033[0;34m' +CYN='\033[0;36m'; RED='\033[0;31m'; BOLD='\033[1m'; NC='\033[0m' + +log() { echo -e "${GRN} ✓${NC} $1"; } +info() { echo -e "${BLU} →${NC} $1"; } +warn() { echo -e "${YLW} !${NC} $1"; } +die() { echo -e "${RED} ✗${NC} $1" >&2; exit 1; } +step() { echo -e "\n${BOLD}${CYN}── $1${NC}"; } + +# ── Flags ───────────────────────────────────────────────────────────────────── +DEMO=false +RESET=false +FORCE_DOCKER=false +for arg in "$@"; do + [[ "$arg" == "--demo" ]] && DEMO=true + [[ "$arg" == "--reset" ]] && RESET=true + [[ "$arg" == "--docker" ]] && FORCE_DOCKER=true +done + +# ── Mode detection ───────────────────────────────────────────────────────────── +has_node() { + command -v node &>/dev/null || return 1 + local ver + ver=$(node -e "process.stdout.write(process.versions.node)" 2>/dev/null) + local major="${ver%%.*}" + [[ "$major" -ge 18 ]] 2>/dev/null +} + +USE_DOCKER=false +if $FORCE_DOCKER; then + USE_DOCKER=true +elif ! has_node; then + warn "Node.js ≥18 not found — switching to Docker mode" + USE_DOCKER=true +fi + +command -v docker &>/dev/null || die "Docker is required. Install Docker Desktop from https://docker.com" + +# ── Docker mode ─────────────────────────────────────────────────────────────── +if $USE_DOCKER; then + step "Docker mode (no Node.js required)" + + command -v docker &>/dev/null || die "Docker not found" + docker info &>/dev/null || die "Docker daemon is not running — start Docker Desktop" + + if $RESET; then + info "Removing existing containers and volumes..." + docker compose --profile full down -v 2>/dev/null || true + log "Cleaned up" + fi + + info "Building image and starting services (this takes ~2 min on first run)..." + $DEMO && info "Demo seed enabled — sample data will be created automatically" + + export DEMO_SEED=$( $DEMO && echo "true" || echo "false" ) + docker compose --profile full up --build -d + + # Wait for the app to be healthy + info "Waiting for app to be ready..." + for i in $(seq 1 30); do + if curl -sf http://localhost:3000/api/leaderboards >/dev/null 2>&1; then break; fi + sleep 2 + done + + echo "" + echo -e "${BOLD}${CYN} ╔══════════════════════════════════════════════════════╗${NC}" + echo -e "${BOLD}${CYN} ║ SCIBASE Community & Reputation ║${NC}" + echo -e "${BOLD}${CYN} ║ http://localhost:3000 ║${NC}" + echo -e "${BOLD}${CYN} ╚══════════════════════════════════════════════════════╝${NC}" + echo "" + echo -e " Logs: ${YLW}docker compose --profile full logs -f app${NC}" + echo -e " Stop: ${YLW}docker compose --profile full down${NC}" + echo "" + exit 0 +fi + +# ── Local mode ──────────────────────────────────────────────────────────────── + +# ── 1. Database ─────────────────────────────────────────────────────────────── +step "Database" + +DB_USER="postgres" +DB_NAME="scibase_community" +POSTGRES_CTR="" + +# Prefer the shared demo-postgres container if it's already running +if docker ps --format "{{.Names}}" 2>/dev/null | grep -q "^demo-postgres$"; then + POSTGRES_CTR="demo-postgres" + DB_PASS="postgres" + log "Using demo-postgres container (postgres / postgres)" +elif docker ps --format "{{.Names}}" 2>/dev/null | grep -qE "community.reputation.*postgres|postgres.*community.reputation"; then + POSTGRES_CTR="$(docker ps --format "{{.Names}}" | grep -E "community.reputation.*postgres|postgres.*community.reputation" | head -1)" + DB_PASS="password" + log "Using existing community-reputation postgres container" +else + command -v docker compose &>/dev/null || die "docker compose not found — start Postgres manually and set DATABASE_URL in .env" + info "Starting postgres via docker compose..." + docker compose up -d postgres + for i in {1..20}; do + docker compose exec -T postgres pg_isready -U postgres &>/dev/null && break + sleep 1 + done + POSTGRES_CTR="$(docker compose ps -q postgres 2>/dev/null | head -1)" + DB_PASS="password" + log "Started community-reputation postgres container" +fi + +DB_URL="postgresql://${DB_USER}:${DB_PASS}@localhost:5432/${DB_NAME}" + +# Create the database if it doesn't exist +if ! docker exec "$POSTGRES_CTR" psql -U "$DB_USER" \ + -tc "SELECT 1 FROM pg_database WHERE datname='$DB_NAME'" 2>/dev/null | grep -q 1; then + docker exec "$POSTGRES_CTR" psql -U "$DB_USER" -c "CREATE DATABASE $DB_NAME;" >/dev/null + log "Created database $DB_NAME" +else + log "Database $DB_NAME already exists" +fi + +# ── 2. .env ─────────────────────────────────────────────────────────────────── +step "Environment" + +if [ ! -f .env ]; then + cat > .env </dev/null +} +generate_prisma || { warn "First attempt failed — retrying..."; generate_prisma; } || \ + die "Prisma client generation failed — run npx prisma generate for full output" +log "Prisma client ready" + +if $RESET; then + info "Resetting database (--reset)..." + DATABASE_URL="$DB_URL" npx prisma migrate reset --force --skip-seed >/dev/null + log "Database reset" +fi + +info "Applying migrations..." +DATABASE_URL="$DB_URL" npx prisma migrate deploy >/dev/null +log "Migrations applied" + +info "Seeding review templates and badges..." +DATABASE_URL="$DB_URL" npx tsx prisma/seed.ts >/dev/null +log "Templates and badges seeded" + +# ── 5. Demo data ────────────────────────────────────────────────────────────── +if $DEMO; then + step "Demo data" + info "Creating sample users, projects, reviews, and endorsements..." + DATABASE_URL="$DB_URL" npx tsx prisma/demo-seed.ts +fi + +# ── 6. Launch ───────────────────────────────────────────────────────────────── +step "Launching" +echo "" +echo -e "${BOLD}${CYN} ╔══════════════════════════════════════════════════════╗${NC}" +echo -e "${BOLD}${CYN} ║ SCIBASE Community & Reputation API ║${NC}" +echo -e "${BOLD}${CYN} ║ http://localhost:3000 (hot-reload dev server) ║${NC}" +echo -e "${BOLD}${CYN} ╚══════════════════════════════════════════════════════╝${NC}" +echo "" +echo -e " ${BOLD}Quick curl tests:${NC}" +echo -e " ${YLW}curl -s 'http://localhost:3000/api/leaderboards' | jq .${NC}" +echo -e " ${YLW}curl -s 'http://localhost:3000/api/leaderboards?domain=biology' | jq .${NC}" +echo -e " ${YLW}curl -s 'http://localhost:3000/api/reviews' | jq .${NC}" +echo "" +if ! $DEMO; then + echo -e " ${YLW}Tip: run ${BOLD}./dev.sh --demo${NC}${YLW} to seed sample data you can browse immediately${NC}" + echo "" +fi + +export DATABASE_URL="$DB_URL" +npm run dev diff --git a/community-reputation/docker-compose.yml b/community-reputation/docker-compose.yml new file mode 100644 index 0000000..0cda811 --- /dev/null +++ b/community-reputation/docker-compose.yml @@ -0,0 +1,43 @@ +version: "3.9" + +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: scibase_community + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./docker/init.sql:/docker-entrypoint-initdb.d/init.sql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + + app: + build: . + ports: + - "3000:3000" + environment: + DATABASE_URL: postgresql://postgres:password@postgres:5432/scibase_community + NEXTAUTH_URL: http://localhost:3000 + NEXTAUTH_SECRET: dev-secret-not-for-production + NEXTAUTH_URL_INTERNAL: http://app:3000 + GITHUB_ID: placeholder + GITHUB_SECRET: placeholder + CRON_SECRET: dev-cron-secret + # Set DEMO_SEED=true in the shell before running to seed sample data: + # DEMO_SEED=true docker compose --profile full up --build + DEMO_SEED: ${DEMO_SEED:-false} + depends_on: + postgres: + condition: service_healthy + profiles: + - full + +volumes: + postgres_data: diff --git a/community-reputation/docker/entrypoint.sh b/community-reputation/docker/entrypoint.sh new file mode 100644 index 0000000..5948510 --- /dev/null +++ b/community-reputation/docker/entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/sh +set -e + +echo "→ Running migrations..." +npx prisma migrate deploy + +echo "→ Seeding templates and badges..." +npx tsx prisma/seed.ts + +if [ "${DEMO_SEED:-false}" = "true" ]; then + echo "→ Seeding demo data..." + npx tsx prisma/demo-seed.ts +fi + +echo "→ Starting server..." +exec npm start diff --git a/community-reputation/docker/init.sql b/community-reputation/docker/init.sql new file mode 100644 index 0000000..5dedaf1 --- /dev/null +++ b/community-reputation/docker/init.sql @@ -0,0 +1,2 @@ +-- Creates the test database alongside the dev database +CREATE DATABASE scibase_community_test; diff --git a/community-reputation/next-env.d.ts b/community-reputation/next-env.d.ts new file mode 100644 index 0000000..4f11a03 --- /dev/null +++ b/community-reputation/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/community-reputation/next.config.mjs b/community-reputation/next.config.mjs new file mode 100644 index 0000000..693b380 --- /dev/null +++ b/community-reputation/next.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + experimental: { + serverComponentsExternalPackages: ["@prisma/client"], + }, +}; + +export default nextConfig; diff --git a/community-reputation/package-lock.json b/community-reputation/package-lock.json new file mode 100644 index 0000000..e21826d --- /dev/null +++ b/community-reputation/package-lock.json @@ -0,0 +1,3551 @@ +{ + "name": "community-reputation", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "community-reputation", + "version": "0.1.0", + "dependencies": { + "@next-auth/prisma-adapter": "^1.0.7", + "@prisma/client": "^5.18.0", + "next": "14.2.5", + "next-auth": "^4.24.7", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^20.14.15", + "@types/react": "^18.3.3", + "@vitest/coverage-v8": "^2.0.5", + "dotenv-cli": "^7.4.2", + "prisma": "^5.18.0", + "tsx": "^4.16.2", + "typescript": "^5.5.4", + "vitest": "^2.0.5" + } + }, + "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/@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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next-auth/prisma-adapter": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@next-auth/prisma-adapter/-/prisma-adapter-1.0.7.tgz", + "integrity": "sha512-Cdko4KfcmKjsyHFrWwZ//lfLUbcLqlyFqjd/nYE2m3aZ7tjMNUjpks47iw7NTCnXf+5UWz5Ypyt1dSs1EP5QJw==", + "license": "ISC", + "peerDependencies": { + "@prisma/client": ">=2.26.0 || >=3", + "next-auth": "^4" + } + }, + "node_modules/@next/env": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz", + "integrity": "sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.5.tgz", + "integrity": "sha512-/9zVxJ+K9lrzSGli1///ujyRfon/ZneeZ+v4ptpiPoOU+GKZnm8Wj8ELWU1Pm7GHltYRBklmXMTUqM/DqQ99FQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.5.tgz", + "integrity": "sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.5.tgz", + "integrity": "sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.5.tgz", + "integrity": "sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.5.tgz", + "integrity": "sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.5.tgz", + "integrity": "sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.5.tgz", + "integrity": "sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.5.tgz", + "integrity": "sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.5.tgz", + "integrity": "sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "tslib": "^2.4.0" + } + }, + "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/node": { + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz", + "integrity": "sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.7", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.12", + "magicast": "^0.3.5", + "std-env": "^3.8.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "2.1.9", + "vitest": "2.1.9" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "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/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "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/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/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/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/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-cli": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-7.4.4.tgz", + "integrity": "sha512-XkBYCG0tPIes+YZr4SpfFv76SQrV/LeCE8CI7JSEMi3VR9MvTihCGTOtbIexD6i2mXF+6px7trb1imVCXSNMDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.6", + "dotenv": "^16.3.0", + "dotenv-expand": "^10.0.0", + "minimist": "^1.2.6" + }, + "bin": { + "dotenv": "cli.js" + } + }, + "node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "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": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/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/glob/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/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/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/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/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-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": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "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/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "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/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "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==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.5.tgz", + "integrity": "sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==", + "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.", + "license": "MIT", + "dependencies": { + "@next/env": "14.2.5", + "@swc/helpers": "0.5.5", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.2.5", + "@next/swc-darwin-x64": "14.2.5", + "@next/swc-linux-arm64-gnu": "14.2.5", + "@next/swc-linux-arm64-musl": "14.2.5", + "@next/swc-linux-x64-gnu": "14.2.5", + "@next/swc-linux-x64-musl": "14.2.5", + "@next/swc-win32-arm64-msvc": "14.2.5", + "@next/swc-win32-ia32-msvc": "14.2.5", + "@next/swc-win32-x64-msvc": "14.2.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-auth": { + "version": "4.24.14", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.14.tgz", + "integrity": "sha512-YRz6xFDXKUwiXSMMChbrBEWyFktZ1qZXEgeSHQQ3nsy08B4c/xLk6REeutRsIFwkjY/1+ShHnu07DN3JeJguig==", + "license": "ISC", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@panva/hkdf": "^1.0.2", + "cookie": "^0.7.0", + "jose": "^4.15.5", + "oauth": "^0.9.15", + "openid-client": "^5.4.0", + "preact": "^10.6.3", + "preact-render-to-string": "^5.1.19", + "uuid": "^8.3.2" + }, + "peerDependencies": { + "@auth/core": "0.34.3", + "next": "^12.2.5 || ^13 || ^14 || ^15 || ^16", + "nodemailer": "^7.0.7", + "react": "^17.0.2 || ^18 || ^19", + "react-dom": "^17.0.2 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@auth/core": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==", + "license": "MIT" + }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/oidc-token-hash": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz", + "integrity": "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==", + "license": "MIT", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, + "node_modules/openid-client": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", + "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", + "license": "MIT", + "dependencies": { + "jose": "^4.15.9", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.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-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "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/preact": { + "version": "10.29.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz", + "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz", + "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==", + "license": "MIT", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, + "node_modules/pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", + "license": "MIT" + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "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.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.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/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "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/string-width-cjs/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/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/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/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/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/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "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" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "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/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "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/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "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": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "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/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "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/wrap-ansi-cjs/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-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/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-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "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-cjs/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/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/community-reputation/package.json b/community-reputation/package.json new file mode 100644 index 0000000..d4db53a --- /dev/null +++ b/community-reputation/package.json @@ -0,0 +1,38 @@ +{ + "name": "community-reputation", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "test": "vitest", + "test:unit": "vitest run tests/unit", + "test:integration": "vitest run tests/integration --fileParallelism=false", + "db:generate": "prisma generate", + "db:migrate": "prisma migrate dev", + "db:migrate:test": "dotenv -e .env.test -- prisma migrate deploy", + "db:seed": "tsx prisma/seed.ts", + "db:studio": "prisma studio" + }, + "dependencies": { + "@next-auth/prisma-adapter": "^1.0.7", + "@prisma/client": "^5.18.0", + "next": "14.2.5", + "next-auth": "^4.24.7", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^20.14.15", + "@types/react": "^18.3.3", + "@vitest/coverage-v8": "^2.0.5", + "dotenv-cli": "^7.4.2", + "prisma": "^5.18.0", + "tsx": "^4.16.2", + "typescript": "^5.5.4", + "vitest": "^2.0.5" + } +} diff --git a/community-reputation/prisma/demo-seed.ts b/community-reputation/prisma/demo-seed.ts new file mode 100644 index 0000000..2cce5b9 --- /dev/null +++ b/community-reputation/prisma/demo-seed.ts @@ -0,0 +1,318 @@ +/** + * Demo seed — creates realistic sample data for local exploration. + * Safe to run multiple times (upserts where possible, skips existing rows). + * + * Run via: DATABASE_URL=... npx tsx prisma/demo-seed.ts + * Or via: ./dev.sh --demo + */ +import { PrismaClient, Prisma } from "@prisma/client"; +import { computeReputation, type ReputationComponents } from "../src/lib/reputation"; + +const prisma = new PrismaClient(); + +// ── helpers ────────────────────────────────────────────────────────────────── + +const upsertUser = (data: { + email: string; + name: string; + institution: string; + domain: string; + region: string; +}) => + prisma.user.upsert({ + where: { email: data.email }, + update: {}, + create: data, + }); + +async function snapshotUser(userId: string) { + const [reviewsCompleted, endorsementsReceived, badgesEarned, reproducibilityVerified] = + await Promise.all([ + prisma.review.count({ where: { reviewerId: userId, status: "PUBLISHED", deletedAt: null } }), + prisma.endorsement.count({ where: { endorseeId: userId, deletedAt: null } }), + prisma.userBadge.count({ where: { userId } }), + prisma.contribution.count({ where: { userId, role: "VALIDATION", deletedAt: null } }), + ]); + + const components: ReputationComponents = { + reviewsCompleted, + endorsementsReceived, + citationsReceived: 0, + forksReceived: 0, + reproducibilityVerified, + bountiesCompleted: 0, + badgesEarned, + }; + const { total, breakdown } = computeReputation(components); + + await prisma.reputationSnapshot.create({ + data: { + userId, + total, + ...components, + breakdown: breakdown as unknown as Prisma.InputJsonValue, + }, + }); + + // Award any newly unlocked badges + const allBadges = await prisma.badge.findMany(); + const existingBadgeSlugs = new Set( + (await prisma.userBadge.findMany({ where: { userId }, include: { badge: true } })).map( + (ub) => ub.badge.slug + ) + ); + for (const badge of allBadges) { + if (existingBadgeSlugs.has(badge.slug)) continue; + const threshold = badge.threshold as { component: string; value: number }; + const value = + threshold.component === "total" + ? total + : (components[threshold.component as keyof ReputationComponents] ?? 0); + if (value >= threshold.value) { + await prisma.userBadge.create({ data: { userId, badgeId: badge.id } }).catch(() => {}); + } + } + + return total; +} + +// ── main ───────────────────────────────────────────────────────────────────── + +async function main() { + // ── Users ───────────────────────────────────────────────────────────────── + const alice = await upsertUser({ + email: "alice.chen@mit.edu", + name: "Alice Chen", + institution: "MIT", + domain: "biology", + region: "us", + }); + const bob = await upsertUser({ + email: "bob.mehta@stanford.edu", + name: "Bob Mehta", + institution: "Stanford", + domain: "physics", + region: "us", + }); + const claire = await upsertUser({ + email: "claire.dupont@oxford.ac.uk", + name: "Claire Dupont", + institution: "University of Oxford", + domain: "social_sciences", + region: "eu", + }); + const diana = await upsertUser({ + email: "diana.osei@harvard.edu", + name: "Diana Osei", + institution: "Harvard", + domain: "biology", + region: "us", + }); + + // ── Projects ────────────────────────────────────────────────────────────── + const aliceProject = await prisma.project.upsert({ + where: { id: "demo-project-alice" }, + update: {}, + create: { + id: "demo-project-alice", + title: "CRISPR off-target effects in human stem cells", + ownerId: alice.id, + }, + }); + const bobProject = await prisma.project.upsert({ + where: { id: "demo-project-bob" }, + update: {}, + create: { + id: "demo-project-bob", + title: "Topological insulators at room temperature", + ownerId: bob.id, + }, + }); + const claireProject = await prisma.project.upsert({ + where: { id: "demo-project-claire" }, + update: {}, + create: { + id: "demo-project-claire", + title: "Social media echo chambers and political polarisation", + ownerId: claire.id, + }, + }); + + // ── Contributions (CRediT) ──────────────────────────────────────────────── + const contrib = async (userId: string, projectId: string, role: string, description?: string) => { + const existing = await prisma.contribution.findFirst({ where: { userId, projectId, role: role as never } }); + if (existing) return existing; + return prisma.contribution.create({ + data: { userId, projectId, role: role as never, description: description ?? null }, + }); + }; + + await contrib(alice.id, aliceProject.id, "CONCEPTUALIZATION", "Led the study design"); + await contrib(alice.id, aliceProject.id, "INVESTIGATION", "Ran all CRISPR experiments"); + await contrib(alice.id, aliceProject.id, "WRITING_ORIGINAL_DRAFT"); + await contrib(diana.id, aliceProject.id, "DATA_CURATION", "Curated sequencing datasets"); + await contrib(diana.id, aliceProject.id, "FORMAL_ANALYSIS", "Statistical analysis of off-target sites"); + await contrib(diana.id, aliceProject.id, "VALIDATION", "Independently reproduced key results"); + + await contrib(bob.id, bobProject.id, "CONCEPTUALIZATION"); + await contrib(bob.id, bobProject.id, "METHODOLOGY", "Developed the synthesis protocol"); + await contrib(bob.id, bobProject.id, "INVESTIGATION"); + await contrib(bob.id, bobProject.id, "WRITING_ORIGINAL_DRAFT"); + + await contrib(claire.id, claireProject.id, "CONCEPTUALIZATION"); + await contrib(claire.id, claireProject.id, "DATA_CURATION", "Built the social graph dataset"); + await contrib(claire.id, claireProject.id, "SOFTWARE", "Implemented network analysis pipeline"); + await contrib(claire.id, claireProject.id, "VISUALIZATION"); + await contrib(claire.id, claireProject.id, "WRITING_ORIGINAL_DRAFT"); + + // ── Reviews ─────────────────────────────────────────────────────────────── + const review = async ( + projectId: string, + reviewerId: string, + content: string, + scores: { clarity: number; rigor: number; novelty: number; reproducibility: number }, + visibility = "PUBLIC" + ) => { + const existing = await prisma.review.findFirst({ where: { projectId, reviewerId, deletedAt: null } }); + if (existing) return existing; + return prisma.review.create({ + data: { + projectId, + reviewerId, + content, + ...scores, + visibility: visibility as never, + status: "PUBLISHED", + }, + }); + }; + + // Bob reviews Alice's project + const r1 = await review( + aliceProject.id, bob.id, + "Rigorous experimental design with clear controls. The off-target analysis is thorough. Minor concern: sample size in Figure 3 is underpowered. Overall a strong contribution to the field.", + { clarity: 4, rigor: 5, novelty: 4, reproducibility: 4 } + ); + // Claire reviews Alice's project (anonymous) + const r2 = await review( + aliceProject.id, claire.id, + "Well-written. The statistical methods could be better described in the methods section. The implications for gene therapy are well-argued.", + { clarity: 4, rigor: 4, novelty: 3, reproducibility: 4 }, + "ANONYMOUS" + ); + // Alice reviews Bob's project + const r3 = await review( + bobProject.id, alice.id, + "Fascinating results. The room-temperature claim needs stronger supporting evidence — the supplemental figures are promising but the confidence intervals are wide.", + { clarity: 5, rigor: 3, novelty: 5, reproducibility: 3 } + ); + // Diana reviews Bob's project (semi-private) + const r4 = await review( + bobProject.id, diana.id, + "The synthesis protocol is detailed and reproducible. I was able to replicate the main result in our lab. The discussion of applications is appropriately cautious.", + { clarity: 5, rigor: 5, novelty: 4, reproducibility: 5 }, + "SEMI_PRIVATE" + ); + // Bob reviews Claire's project + const r5 = await review( + claireProject.id, bob.id, + "Solid methodology. The network analysis pipeline is well-documented. The causal claims about polarisation need more careful hedging given the observational design.", + { clarity: 4, rigor: 4, novelty: 4, reproducibility: 4 } + ); + + // ── Comments ────────────────────────────────────────────────────────────── + const comment = async (authorId: string, targetId: string, body: string, parentId?: string) => { + const existing = await prisma.comment.findFirst({ + where: { authorId, targetId, body, targetType: "REVIEW", deletedAt: null }, + }); + if (existing) return existing; + return prisma.comment.create({ + data: { authorId, body, targetType: "REVIEW", targetId, parentId: parentId ?? null }, + }); + }; + + const c1 = await comment(alice.id, r1.id, "Thanks for the feedback on Figure 3 — we're running additional replicates and will update the manuscript."); + await comment(bob.id, r1.id, "Happy to review the updated version once you have more data.", c1.id); + await comment(diana.id, r4.id, "Glad you could replicate it! What cell line did you use?"); + + // ── Endorsements ────────────────────────────────────────────────────────── + const endorse = async (endorserId: string, endorseeId: string, skill: string, note?: string) => { + try { + await prisma.endorsement.upsert({ + where: { endorserId_endorseeId_skill: { endorserId, endorseeId, skill } }, + create: { endorserId, endorseeId, skill, note: note ?? null }, + update: { deletedAt: null }, + }); + } catch { + // skip if already exists and unique constraint fires + } + }; + + await endorse(bob.id, alice.id, "experimental-biology", "Alice's CRISPR work is meticulous."); + await endorse(diana.id, alice.id, "experimental-biology"); + await endorse(claire.id, alice.id, "scientific-writing"); + await endorse(alice.id, bob.id, "condensed-matter-physics"); + await endorse(diana.id, bob.id, "reproducibility"); + await endorse(alice.id, claire.id, "network-analysis"); + await endorse(bob.id, claire.id, "data-visualisation"); + await endorse(alice.id, diana.id, "statistical-analysis"); + await endorse(bob.id, diana.id, "reproducibility"); + await endorse(claire.id, diana.id, "statistical-analysis"); + + // ── Admin badge for Alice ───────────────────────────────────────────────── + const adminBadge = await prisma.badge.findUnique({ where: { slug: "admin" } }); + if (adminBadge) { + await prisma.userBadge + .upsert({ + where: { userId_badgeId: { userId: alice.id, badgeId: adminBadge.id } }, + create: { userId: alice.id, badgeId: adminBadge.id }, + update: {}, + }) + .catch(() => {}); + } + + // ── Reputation snapshots ────────────────────────────────────────────────── + const scores: Record = {}; + for (const [name, user] of [["Alice", alice], ["Bob", bob], ["Claire", claire], ["Diana", diana]] as const) { + scores[name] = await snapshotUser(user.id); + } + + // ── Summary ─────────────────────────────────────────────────────────────── + console.log("\n Demo data ready.\n"); + console.log(" ┌─────────────────────────────────────────────────────────────┐"); + console.log(" │ User IDs (copy these into curl commands below) │"); + console.log(" ├─────────────────────────────────────────────────────────────┤"); + console.log(` │ Alice Chen (biology/MIT) ${alice.id} │`); + console.log(` │ Bob Mehta (physics/Stanford) ${bob.id} │`); + console.log(` │ Claire Dupont (social/Oxford) ${claire.id} │`); + console.log(` │ Diana Osei (biology/Harvard) ${diana.id} │`); + console.log(" ├─────────────────────────────────────────────────────────────┤"); + console.log(" │ Project IDs │"); + console.log(" ├─────────────────────────────────────────────────────────────┤"); + console.log(` │ CRISPR study demo-project-alice │`); + console.log(` │ Topological insulators demo-project-bob │`); + console.log(` │ Social media study demo-project-claire │`); + console.log(" └─────────────────────────────────────────────────────────────┘"); + console.log(""); + console.log(" Reputation scores:"); + for (const [name, score] of Object.entries(scores)) { + const bar = "█".repeat(Math.max(1, Math.round(score / 3))); + console.log(` ${name.padEnd(10)} ${score.toFixed(2).padStart(6)} ${bar}`); + } + console.log(""); + console.log(" Try once the server is up:"); + console.log(" curl -s 'http://localhost:3000/api/leaderboards' | jq ."); + console.log(" curl -s 'http://localhost:3000/api/leaderboards?domain=biology' | jq ."); + console.log(` curl -s 'http://localhost:3000/api/reputation/${alice.id}' | jq .`); + console.log(` curl -s 'http://localhost:3000/api/reputation/${alice.id}/history' | jq .`); + console.log(` curl -s 'http://localhost:3000/api/users/${alice.id}/badges' | jq .`); + console.log(` curl -s 'http://localhost:3000/api/reviews?projectId=demo-project-alice' | jq .`); + console.log(` curl -s 'http://localhost:3000/api/contributions?userId=${alice.id}' | jq .`); + console.log(` curl -s 'http://localhost:3000/api/contributions/export?userId=${alice.id}'`); + console.log(` curl -s 'http://localhost:3000/api/endorsements?endorseeId=${alice.id}' | jq .`); + console.log(""); +} + +main() + .catch((e) => { console.error(e); process.exit(1); }) + .finally(() => prisma.$disconnect()); diff --git a/community-reputation/prisma/migrations/20260428235308_init_community_reputation/migration.sql b/community-reputation/prisma/migrations/20260428235308_init_community_reputation/migration.sql new file mode 100644 index 0000000..726ae37 --- /dev/null +++ b/community-reputation/prisma/migrations/20260428235308_init_community_reputation/migration.sql @@ -0,0 +1,298 @@ +-- CreateEnum +CREATE TYPE "ReviewVisibility" AS ENUM ('PUBLIC', 'SEMI_PRIVATE', 'ANONYMOUS', 'DOUBLE_BLIND'); + +-- CreateEnum +CREATE TYPE "ReviewStatus" AS ENUM ('DRAFT', 'PUBLISHED', 'ARCHIVED'); + +-- CreateEnum +CREATE TYPE "CommentTargetType" AS ENUM ('DOCUMENT', 'DATASET', 'CODE_BLOCK', 'NOTEBOOK_CELL', 'REVIEW', 'PROJECT'); + +-- CreateEnum +CREATE TYPE "CreditRole" AS ENUM ('CONCEPTUALIZATION', 'DATA_CURATION', 'FORMAL_ANALYSIS', 'FUNDING_ACQUISITION', 'INVESTIGATION', 'METHODOLOGY', 'PROJECT_ADMINISTRATION', 'RESOURCES', 'SOFTWARE', 'SUPERVISION', 'VALIDATION', 'VISUALIZATION', 'WRITING_ORIGINAL_DRAFT', 'WRITING_REVIEW_EDITING'); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "name" TEXT, + "image" TEXT, + "institution" TEXT, + "domain" TEXT, + "region" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Account" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "providerAccountId" TEXT NOT NULL, + "refresh_token" TEXT, + "access_token" TEXT, + "expires_at" INTEGER, + "token_type" TEXT, + "scope" TEXT, + "id_token" TEXT, + "session_state" TEXT, + + CONSTRAINT "Account_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Session" ( + "id" TEXT NOT NULL, + "sessionToken" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Session_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "VerificationToken" ( + "identifier" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL +); + +-- CreateTable +CREATE TABLE "Project" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "ownerId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Project_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ReviewTemplate" ( + "id" TEXT NOT NULL, + "discipline" TEXT NOT NULL, + "name" TEXT NOT NULL, + "fields" JSONB NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ReviewTemplate_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Review" ( + "id" TEXT NOT NULL, + "projectId" TEXT NOT NULL, + "reviewerId" TEXT NOT NULL, + "templateId" TEXT, + "clarity" INTEGER, + "rigor" INTEGER, + "novelty" INTEGER, + "reproducibility" INTEGER, + "content" TEXT NOT NULL, + "visibility" "ReviewVisibility" NOT NULL DEFAULT 'PUBLIC', + "status" "ReviewStatus" NOT NULL DEFAULT 'DRAFT', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + + CONSTRAINT "Review_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Comment" ( + "id" TEXT NOT NULL, + "authorId" TEXT NOT NULL, + "body" TEXT NOT NULL, + "anchor" JSONB, + "targetType" "CommentTargetType" NOT NULL, + "targetId" TEXT NOT NULL, + "parentId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + + CONSTRAINT "Comment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Contribution" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "projectId" TEXT NOT NULL, + "role" "CreditRole" NOT NULL, + "description" TEXT, + "occurredAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + + CONSTRAINT "Contribution_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Endorsement" ( + "id" TEXT NOT NULL, + "endorserId" TEXT NOT NULL, + "endorseeId" TEXT NOT NULL, + "skill" TEXT NOT NULL, + "note" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deletedAt" TIMESTAMP(3), + + CONSTRAINT "Endorsement_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Badge" ( + "id" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "threshold" JSONB NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Badge_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserBadge" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "badgeId" TEXT NOT NULL, + "awardedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "UserBadge_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ReputationSnapshot" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "total" DOUBLE PRECISION NOT NULL, + "reviewsCompleted" INTEGER NOT NULL, + "endorsementsReceived" INTEGER NOT NULL, + "citationsReceived" INTEGER NOT NULL, + "forksReceived" INTEGER NOT NULL, + "reproducibilityVerified" INTEGER NOT NULL, + "bountiesCompleted" INTEGER NOT NULL DEFAULT 0, + "badgesEarned" INTEGER NOT NULL, + "breakdown" JSONB NOT NULL, + "computedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ReputationSnapshot_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE INDEX "Account_userId_idx" ON "Account"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); + +-- CreateIndex +CREATE INDEX "Session_userId_idx" ON "Session"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token"); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token"); + +-- CreateIndex +CREATE INDEX "Project_ownerId_idx" ON "Project"("ownerId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ReviewTemplate_discipline_name_key" ON "ReviewTemplate"("discipline", "name"); + +-- CreateIndex +CREATE INDEX "Review_projectId_idx" ON "Review"("projectId"); + +-- CreateIndex +CREATE INDEX "Review_reviewerId_idx" ON "Review"("reviewerId"); + +-- CreateIndex +CREATE INDEX "Review_status_idx" ON "Review"("status"); + +-- CreateIndex +CREATE INDEX "Comment_targetType_targetId_idx" ON "Comment"("targetType", "targetId"); + +-- CreateIndex +CREATE INDEX "Comment_authorId_idx" ON "Comment"("authorId"); + +-- CreateIndex +CREATE INDEX "Contribution_userId_projectId_idx" ON "Contribution"("userId", "projectId"); + +-- CreateIndex +CREATE INDEX "Contribution_projectId_idx" ON "Contribution"("projectId"); + +-- CreateIndex +CREATE INDEX "Endorsement_endorseeId_idx" ON "Endorsement"("endorseeId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Endorsement_endorserId_endorseeId_skill_key" ON "Endorsement"("endorserId", "endorseeId", "skill"); + +-- CreateIndex +CREATE UNIQUE INDEX "Badge_slug_key" ON "Badge"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserBadge_userId_badgeId_key" ON "UserBadge"("userId", "badgeId"); + +-- CreateIndex +CREATE INDEX "ReputationSnapshot_userId_computedAt_idx" ON "ReputationSnapshot"("userId", "computedAt"); + +-- CreateIndex +CREATE INDEX "ReputationSnapshot_total_idx" ON "ReputationSnapshot"("total"); + +-- AddForeignKey +ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Project" ADD CONSTRAINT "Project_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Review" ADD CONSTRAINT "Review_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Review" ADD CONSTRAINT "Review_reviewerId_fkey" FOREIGN KEY ("reviewerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Review" ADD CONSTRAINT "Review_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "ReviewTemplate"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Comment"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Contribution" ADD CONSTRAINT "Contribution_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Contribution" ADD CONSTRAINT "Contribution_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Endorsement" ADD CONSTRAINT "Endorsement_endorserId_fkey" FOREIGN KEY ("endorserId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Endorsement" ADD CONSTRAINT "Endorsement_endorseeId_fkey" FOREIGN KEY ("endorseeId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserBadge" ADD CONSTRAINT "UserBadge_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserBadge" ADD CONSTRAINT "UserBadge_badgeId_fkey" FOREIGN KEY ("badgeId") REFERENCES "Badge"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ReputationSnapshot" ADD CONSTRAINT "ReputationSnapshot_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/community-reputation/prisma/migrations/migration_lock.toml b/community-reputation/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/community-reputation/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/community-reputation/prisma/schema.prisma b/community-reputation/prisma/schema.prisma new file mode 100644 index 0000000..9e50880 --- /dev/null +++ b/community-reputation/prisma/schema.prisma @@ -0,0 +1,299 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// --------------------------------------------------------------------------- +// Stub models — replace with your auth provider's User once it's wired in. +// --------------------------------------------------------------------------- + +model User { + id String @id @default(cuid()) + email String @unique + name String? + image String? + institution String? + domain String? // e.g. "biology", "physics", "social_sciences" + region String? // e.g. "us", "eu", "asia" + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // NextAuth.js relations + accounts Account[] + sessions Session[] + + reviewsWritten Review[] @relation("ReviewerRelation") + commentsWritten Comment[] + contributions Contribution[] + endorsementsGiven Endorsement[] @relation("EndorserRelation") + endorsementsReceived Endorsement[] @relation("EndorseeRelation") + badges UserBadge[] + reputationSnapshots ReputationSnapshot[] + projectsOwned Project[] +} + +// --------------------------------------------------------------------------- +// NextAuth.js — Prisma Adapter required models +// --------------------------------------------------------------------------- + +model Account { + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? @db.Text + access_token String? @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? @db.Text + session_state String? + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) + @@index([userId]) +} + +model Session { + id String @id @default(cuid()) + sessionToken String @unique + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) +} + +model VerificationToken { + identifier String + token String @unique + expires DateTime + + @@unique([identifier, token]) +} + +model Project { + id String @id @default(cuid()) + title String + ownerId String + owner User @relation(fields: [ownerId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + reviews Review[] + contributions Contribution[] + + @@index([ownerId]) +} + +// --------------------------------------------------------------------------- +// Peer Reviews +// --------------------------------------------------------------------------- + +enum ReviewVisibility { + PUBLIC + SEMI_PRIVATE // reviewer visible only to project authors and the reviewer + ANONYMOUS // reviewer visible only to themselves; authors see the content + DOUBLE_BLIND // reviewer and author identities hidden from each other +} + +enum ReviewStatus { + DRAFT + PUBLISHED + ARCHIVED +} + +model ReviewTemplate { + id String @id @default(cuid()) + discipline String + name String + fields Json // TemplateField[] — see src/lib/review-templates.ts + createdAt DateTime @default(now()) + + reviews Review[] + + @@unique([discipline, name]) +} + +model Review { + id String @id @default(cuid()) + projectId String + project Project @relation(fields: [projectId], references: [id]) + reviewerId String + reviewer User @relation("ReviewerRelation", fields: [reviewerId], references: [id]) + templateId String? + template ReviewTemplate? @relation(fields: [templateId], references: [id]) + + // Optional structured scores (1–5) + clarity Int? + rigor Int? + novelty Int? + reproducibility Int? + content String + + visibility ReviewVisibility @default(PUBLIC) + status ReviewStatus @default(DRAFT) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? // soft-delete; never hard-delete to preserve contribution graphs + + @@index([projectId]) + @@index([reviewerId]) + @@index([status]) +} + +// --------------------------------------------------------------------------- +// Inline Comments (polymorphic) +// --------------------------------------------------------------------------- + +enum CommentTargetType { + DOCUMENT + DATASET + CODE_BLOCK + NOTEBOOK_CELL + REVIEW + PROJECT +} + +model Comment { + id String @id @default(cuid()) + authorId String + author User @relation(fields: [authorId], references: [id]) + body String + anchor Json? // inline anchor: { start, end } | { line } | { cell, offset } etc. + targetType CommentTargetType + targetId String // ID of the target entity + parentId String? // for threaded replies + parent Comment? @relation("CommentReplies", fields: [parentId], references: [id]) + replies Comment[] @relation("CommentReplies") + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + @@index([targetType, targetId]) + @@index([authorId]) +} + +// --------------------------------------------------------------------------- +// Contributor Credits (CRediT taxonomy) +// --------------------------------------------------------------------------- + +enum CreditRole { + CONCEPTUALIZATION + DATA_CURATION + FORMAL_ANALYSIS + FUNDING_ACQUISITION + INVESTIGATION + METHODOLOGY + PROJECT_ADMINISTRATION + RESOURCES + SOFTWARE + SUPERVISION + VALIDATION + VISUALIZATION + WRITING_ORIGINAL_DRAFT + WRITING_REVIEW_EDITING +} + +model Contribution { + id String @id @default(cuid()) + userId String + user User @relation(fields: [userId], references: [id]) + projectId String + project Project @relation(fields: [projectId], references: [id]) + role CreditRole + description String? + occurredAt DateTime @default(now()) // accepts backfilled dates for legacy data + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + @@index([userId, projectId]) + @@index([projectId]) +} + +// --------------------------------------------------------------------------- +// Endorsements +// --------------------------------------------------------------------------- + +model Endorsement { + id String @id @default(cuid()) + endorserId String + endorser User @relation("EndorserRelation", fields: [endorserId], references: [id]) + endorseeId String + endorsee User @relation("EndorseeRelation", fields: [endorseeId], references: [id]) + skill String // e.g. "peer-review", "reproducibility", "data-analysis" + note String? + + createdAt DateTime @default(now()) + deletedAt DateTime? // soft-delete so history is preserved + + @@unique([endorserId, endorseeId, skill]) // prevents duplicate endorsements + @@index([endorseeId]) +} + +// --------------------------------------------------------------------------- +// Badges +// --------------------------------------------------------------------------- + +model Badge { + id String @id @default(cuid()) + slug String @unique + name String + description String + // threshold: { component: keyof ReputationComponents | "total", value: number } + threshold Json + + createdAt DateTime @default(now()) + + userBadges UserBadge[] +} + +model UserBadge { + id String @id @default(cuid()) + userId String + user User @relation(fields: [userId], references: [id]) + badgeId String + badge Badge @relation(fields: [badgeId], references: [id]) + awardedAt DateTime @default(now()) + + @@unique([userId, badgeId]) // each badge awarded at most once per user +} + +// --------------------------------------------------------------------------- +// Reputation Snapshots (append-only; never update rows) +// --------------------------------------------------------------------------- + +model ReputationSnapshot { + id String @id @default(cuid()) + userId String + user User @relation(fields: [userId], references: [id]) + + total Float + + // Raw component counts at snapshot time + reviewsCompleted Int + endorsementsReceived Int + citationsReceived Int + forksReceived Int + reproducibilityVerified Int + bountiesCompleted Int @default(0) + badgesEarned Int + + // ReputationBreakdown[] — component, rawValue, smoothed, weighted + breakdown Json + + computedAt DateTime @default(now()) + + @@index([userId, computedAt]) + @@index([total]) // for leaderboard ORDER BY +} diff --git a/community-reputation/prisma/seed.ts b/community-reputation/prisma/seed.ts new file mode 100644 index 0000000..a115c05 --- /dev/null +++ b/community-reputation/prisma/seed.ts @@ -0,0 +1,67 @@ +import { PrismaClient } from "@prisma/client"; +import { REVIEW_TEMPLATES } from "../src/lib/review-templates"; + +const prisma = new PrismaClient(); + +async function main() { + console.log("Seeding review templates..."); + for (const t of REVIEW_TEMPLATES) { + await prisma.reviewTemplate.upsert({ + where: { discipline_name: { discipline: t.discipline, name: t.name } }, + update: { fields: t.fields as object[] }, + create: { discipline: t.discipline, name: t.name, fields: t.fields as object[] }, + }); + } + + console.log("Seeding badges..."); + const badges = [ + { + slug: "trusted_reviewer", + name: "Trusted Reviewer", + description: "Completed 10 or more published peer reviews", + threshold: { component: "reviewsCompleted", value: 10 }, + }, + { + slug: "open_science_champion", + name: "Open Science Champion", + description: "Received 20 or more peer endorsements", + threshold: { component: "endorsementsReceived", value: 20 }, + }, + { + slug: "reproducibility_verified", + name: "Reproducibility Verified", + description: "Achieved a total reputation score of 50 or higher", + threshold: { component: "total", value: 50 }, + }, + { + slug: "prolific_contributor", + name: "Prolific Contributor", + description: "Work cited 10 or more times", + threshold: { component: "citationsReceived", value: 10 }, + }, + { + slug: "admin", + name: "Admin", + // Threshold set impossibly high — awarded manually in demo-seed, never auto-unlocked. + description: "SCIBASE platform administrator", + threshold: { component: "total", value: 999999 }, + }, + ]; + + for (const badge of badges) { + await prisma.badge.upsert({ + where: { slug: badge.slug }, + update: { name: badge.name, description: badge.description, threshold: badge.threshold }, + create: badge, + }); + } + + console.log("Seed complete."); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(() => prisma.$disconnect()); diff --git a/community-reputation/src/app/api/auth/[...nextauth]/route.ts b/community-reputation/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..9cd7923 --- /dev/null +++ b/community-reputation/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,5 @@ +import NextAuth from "next-auth"; +import { authOptions } from "@/lib/auth"; + +const handler = NextAuth(authOptions); +export { handler as GET, handler as POST }; diff --git a/community-reputation/src/app/api/comments/[id]/route.ts b/community-reputation/src/app/api/comments/[id]/route.ts new file mode 100644 index 0000000..17a2493 --- /dev/null +++ b/community-reputation/src/app/api/comments/[id]/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import { requireAuth } from "@/lib/auth"; +import { handleError } from "@/lib/api"; + +// Soft-delete a comment. Only the author may delete their own comment. +export async function DELETE(_req: NextRequest, { params }: { params: { id: string } }) { + try { + const { userId } = await requireAuth(); + + const comment = await prisma.comment.findFirst({ + where: { id: params.id, deletedAt: null }, + select: { authorId: true }, + }); + if (!comment) return NextResponse.json({ error: "Comment not found" }, { status: 404 }); + if (comment.authorId !== userId) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + await prisma.comment.update({ + where: { id: params.id }, + data: { deletedAt: new Date() }, + }); + + return new NextResponse(null, { status: 204 }); + } catch (e) { + return handleError(e); + } +} diff --git a/community-reputation/src/app/api/comments/route.ts b/community-reputation/src/app/api/comments/route.ts new file mode 100644 index 0000000..f748b4d --- /dev/null +++ b/community-reputation/src/app/api/comments/route.ts @@ -0,0 +1,50 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; +import { CommentTargetType } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; +import { requireAuth } from "@/lib/auth"; +import { handleError } from "@/lib/api"; +import { addComment, listComments } from "@/services/comment.service"; + +const CreateCommentSchema = z.object({ + body: z.string().min(1).max(10000), + anchor: z.record(z.unknown()).optional().transform((v) => v as import("@prisma/client").Prisma.InputJsonValue | undefined), + targetType: z.nativeEnum(CommentTargetType), + targetId: z.string().min(1), + parentId: z.string().cuid().optional(), +}); + +export async function POST(req: NextRequest) { + try { + const { userId } = await requireAuth(); + const body = CreateCommentSchema.parse(await req.json()); + const comment = await addComment(prisma, { ...body, authorId: userId }); + return NextResponse.json(comment, { status: 201 }); + } catch (e) { + return handleError(e); + } +} + +export async function GET(req: NextRequest) { + try { + const { searchParams } = new URL(req.url); + const targetType = searchParams.get("targetType"); + const targetId = searchParams.get("targetId"); + + if (!targetType || !targetId) { + return NextResponse.json( + { error: "targetType and targetId query params are required" }, + { status: 400 } + ); + } + + if (!Object.values(CommentTargetType).includes(targetType as CommentTargetType)) { + return NextResponse.json({ error: "Invalid targetType" }, { status: 422 }); + } + + const comments = await listComments(prisma, targetType as CommentTargetType, targetId); + return NextResponse.json(comments); + } catch (e) { + return handleError(e); + } +} diff --git a/community-reputation/src/app/api/contributions/[id]/route.ts b/community-reputation/src/app/api/contributions/[id]/route.ts new file mode 100644 index 0000000..4855490 --- /dev/null +++ b/community-reputation/src/app/api/contributions/[id]/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import { requireAuth } from "@/lib/auth"; +import { handleError } from "@/lib/api"; + +// Soft-delete a contribution. Only the contributor may retract their own record. +export async function DELETE(_req: NextRequest, { params }: { params: { id: string } }) { + try { + const { userId } = await requireAuth(); + + const contribution = await prisma.contribution.findFirst({ + where: { id: params.id, deletedAt: null }, + select: { userId: true }, + }); + if (!contribution) return NextResponse.json({ error: "Contribution not found" }, { status: 404 }); + if (contribution.userId !== userId) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + await prisma.contribution.update({ + where: { id: params.id }, + data: { deletedAt: new Date() }, + }); + + return new NextResponse(null, { status: 204 }); + } catch (e) { + return handleError(e); + } +} diff --git a/community-reputation/src/app/api/contributions/export/route.ts b/community-reputation/src/app/api/contributions/export/route.ts new file mode 100644 index 0000000..a088b26 --- /dev/null +++ b/community-reputation/src/app/api/contributions/export/route.ts @@ -0,0 +1,68 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import { handleError } from "@/lib/api"; +import { CREDIT_LABELS, CreditRole } from "@/lib/credit"; + +function escapeCSV(value: string | null | undefined): string { + if (value == null) return ""; + const str = String(value); + if (str.includes(",") || str.includes('"') || str.includes("\n")) { + return `"${str.replace(/"/g, '""')}"`; + } + return str; +} + +// GET /api/contributions/export?userId= +// Returns a CRediT-aligned CSV suitable for tenure/promotion applications. +export async function GET(req: NextRequest) { + try { + const { searchParams } = new URL(req.url); + const userId = searchParams.get("userId"); + if (!userId) { + return NextResponse.json({ error: "userId query param is required" }, { status: 400 }); + } + + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { id: true, name: true, email: true }, + }); + if (!user) return NextResponse.json({ error: "User not found" }, { status: 404 }); + + const contributions = await prisma.contribution.findMany({ + where: { userId, deletedAt: null }, + include: { project: { select: { id: true, title: true } } }, + orderBy: { occurredAt: "asc" }, + }); + + const header = [ + "project_title", + "project_id", + "credit_role", + "credit_role_label", + "description", + "occurred_at", + ].join(","); + + const rows = contributions.map((c) => + [ + escapeCSV(c.project.title), + escapeCSV(c.project.id), + escapeCSV(c.role), + escapeCSV(CREDIT_LABELS[c.role as unknown as CreditRole] ?? c.role), + escapeCSV(c.description), + escapeCSV(c.occurredAt.toISOString()), + ].join(",") + ); + + const csv = [header, ...rows].join("\n"); + + return new NextResponse(csv, { + headers: { + "Content-Type": "text/csv; charset=utf-8", + "Content-Disposition": `attachment; filename="credit-contributions-${userId}.csv"`, + }, + }); + } catch (e) { + return handleError(e); + } +} diff --git a/community-reputation/src/app/api/contributions/route.ts b/community-reputation/src/app/api/contributions/route.ts new file mode 100644 index 0000000..949b969 --- /dev/null +++ b/community-reputation/src/app/api/contributions/route.ts @@ -0,0 +1,38 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; +import { CreditRole } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; +import { requireAuth } from "@/lib/auth"; +import { handleError } from "@/lib/api"; +import { logContribution, listContributions } from "@/services/contribution.service"; + +const CreateContributionSchema = z.object({ + projectId: z.string().min(1), + role: z.nativeEnum(CreditRole), + description: z.string().max(500).optional(), + occurredAt: z.coerce.date().optional(), +}); + +export async function POST(req: NextRequest) { + try { + const { userId } = await requireAuth(); + const body = CreateContributionSchema.parse(await req.json()); + const contribution = await logContribution(prisma, { ...body, userId }); + return NextResponse.json(contribution, { status: 201 }); + } catch (e) { + return handleError(e); + } +} + +export async function GET(req: NextRequest) { + try { + const { searchParams } = new URL(req.url); + const contributions = await listContributions(prisma, { + userId: searchParams.get("userId") ?? undefined, + projectId: searchParams.get("projectId") ?? undefined, + }); + return NextResponse.json(contributions); + } catch (e) { + return handleError(e); + } +} diff --git a/community-reputation/src/app/api/cron/reputation-snapshots/route.ts b/community-reputation/src/app/api/cron/reputation-snapshots/route.ts new file mode 100644 index 0000000..7f12628 --- /dev/null +++ b/community-reputation/src/app/api/cron/reputation-snapshots/route.ts @@ -0,0 +1,26 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import { writeSnapshot } from "@/services/reputation.service"; + +// Triggered by Vercel Cron or any scheduler via: +// GET /api/cron/reputation-snapshots +// Header: x-cron-secret: +// +// Writes a ReputationSnapshot row per active user and awards any newly +// unlocked badges. Safe to re-run — snapshots are append-only. +export async function GET(req: NextRequest) { + const secret = req.headers.get("x-cron-secret"); + if (!process.env.CRON_SECRET || secret !== process.env.CRON_SECRET) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const users = await prisma.user.findMany({ select: { id: true } }); + const results = await Promise.allSettled(users.map((u) => writeSnapshot(prisma, u.id))); + + const succeeded = results.filter((r) => r.status === "fulfilled").length; + const failed = results + .filter((r): r is PromiseRejectedResult => r.status === "rejected") + .map((r) => r.reason?.message ?? "unknown"); + + return NextResponse.json({ succeeded, failedCount: failed.length, errors: failed }); +} diff --git a/community-reputation/src/app/api/endorsements/route.ts b/community-reputation/src/app/api/endorsements/route.ts new file mode 100644 index 0000000..fae0164 --- /dev/null +++ b/community-reputation/src/app/api/endorsements/route.ts @@ -0,0 +1,63 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; +import { prisma } from "@/lib/prisma"; +import { requireAuth } from "@/lib/auth"; +import { handleError } from "@/lib/api"; +import { + createEndorsement, + removeEndorsement, + listEndorsements, +} from "@/services/endorsement.service"; + +const CreateEndorsementSchema = z.object({ + endorseeId: z.string().cuid(), + skill: z.string().min(1).max(100), + note: z.string().max(500).optional(), +}); + +const DeleteEndorsementSchema = z.object({ + endorseeId: z.string().cuid(), + skill: z.string().min(1).max(100), +}); + +export async function POST(req: NextRequest) { + try { + const { userId } = await requireAuth(); + const body = CreateEndorsementSchema.parse(await req.json()); + const endorsement = await createEndorsement( + prisma, + userId, + body.endorseeId, + body.skill, + body.note + ); + return NextResponse.json(endorsement, { status: 201 }); + } catch (e) { + return handleError(e); + } +} + +export async function DELETE(req: NextRequest) { + try { + const { userId } = await requireAuth(); + const body = DeleteEndorsementSchema.parse(await req.json()); + await removeEndorsement(prisma, userId, body.endorseeId, body.skill); + return new NextResponse(null, { status: 204 }); + } catch (e) { + return handleError(e); + } +} + +export async function GET(req: NextRequest) { + try { + const { searchParams } = new URL(req.url); + const endorseeId = searchParams.get("endorseeId"); + if (!endorseeId) { + return NextResponse.json({ error: "endorseeId query param is required" }, { status: 400 }); + } + const endorsements = await listEndorsements(prisma, endorseeId); + return NextResponse.json(endorsements); + } catch (e) { + return handleError(e); + } +} diff --git a/community-reputation/src/app/api/leaderboards/route.ts b/community-reputation/src/app/api/leaderboards/route.ts new file mode 100644 index 0000000..96c7709 --- /dev/null +++ b/community-reputation/src/app/api/leaderboards/route.ts @@ -0,0 +1,23 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; +import { prisma } from "@/lib/prisma"; +import { handleError } from "@/lib/api"; +import { getLeaderboard } from "@/services/reputation.service"; + +const LeaderboardQuerySchema = z.object({ + domain: z.string().optional(), + region: z.string().optional(), + institution: z.string().optional(), + limit: z.coerce.number().int().min(1).max(100).default(50), +}); + +export async function GET(req: NextRequest) { + try { + const { searchParams } = new URL(req.url); + const query = LeaderboardQuerySchema.parse(Object.fromEntries(searchParams)); + const entries = await getLeaderboard(prisma, query, query.limit); + return NextResponse.json(entries); + } catch (e) { + return handleError(e); + } +} diff --git a/community-reputation/src/app/api/reputation/[userId]/history/route.ts b/community-reputation/src/app/api/reputation/[userId]/history/route.ts new file mode 100644 index 0000000..408b88b --- /dev/null +++ b/community-reputation/src/app/api/reputation/[userId]/history/route.ts @@ -0,0 +1,45 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; +import { prisma } from "@/lib/prisma"; +import { handleError } from "@/lib/api"; + +const QuerySchema = z.object({ + limit: z.coerce.number().int().min(1).max(365).default(90), +}); + +// Returns the append-only snapshot history for a user — used by score sparklines. +export async function GET(req: NextRequest, { params }: { params: { userId: string } }) { + try { + const user = await prisma.user.findUnique({ + where: { id: params.userId }, + select: { id: true }, + }); + if (!user) return NextResponse.json({ error: "User not found" }, { status: 404 }); + + const query = QuerySchema.safeParse(Object.fromEntries(new URL(req.url).searchParams)); + if (!query.success) { + return NextResponse.json({ error: "Invalid query params", details: query.error.errors }, { status: 422 }); + } + + const snapshots = await prisma.reputationSnapshot.findMany({ + where: { userId: params.userId }, + select: { + total: true, + computedAt: true, + reviewsCompleted: true, + endorsementsReceived: true, + citationsReceived: true, + forksReceived: true, + reproducibilityVerified: true, + bountiesCompleted: true, + badgesEarned: true, + }, + orderBy: { computedAt: "asc" }, + take: query.data.limit, + }); + + return NextResponse.json(snapshots); + } catch (e) { + return handleError(e); + } +} diff --git a/community-reputation/src/app/api/reputation/[userId]/route.ts b/community-reputation/src/app/api/reputation/[userId]/route.ts new file mode 100644 index 0000000..c2476ee --- /dev/null +++ b/community-reputation/src/app/api/reputation/[userId]/route.ts @@ -0,0 +1,24 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import { handleError } from "@/lib/api"; +import { gatherComponents } from "@/services/reputation.service"; +import { computeReputation } from "@/lib/reputation"; + +// Returns a live reputation score + full breakdown for a user. +// Does NOT write a snapshot — that is the cron job's responsibility. +export async function GET(_req: NextRequest, { params }: { params: { userId: string } }) { + try { + const user = await prisma.user.findUnique({ + where: { id: params.userId }, + select: { id: true, name: true, institution: true, domain: true }, + }); + if (!user) return NextResponse.json({ error: "User not found" }, { status: 404 }); + + const components = await gatherComponents(prisma, params.userId); + const { total, breakdown } = computeReputation(components); + + return NextResponse.json({ user, total, components, breakdown }); + } catch (e) { + return handleError(e); + } +} diff --git a/community-reputation/src/app/api/reviews/[id]/route.ts b/community-reputation/src/app/api/reviews/[id]/route.ts new file mode 100644 index 0000000..e0a8eaf --- /dev/null +++ b/community-reputation/src/app/api/reviews/[id]/route.ts @@ -0,0 +1,64 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; +import { ReviewVisibility, ReviewStatus } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; +import { requireAuth, getAuth } from "@/lib/auth"; +import { handleError } from "@/lib/api"; +import { getReview, updateReview } from "@/services/review.service"; + +const PatchReviewSchema = z + .object({ + clarity: z.number().int().min(1).max(5).optional(), + rigor: z.number().int().min(1).max(5).optional(), + novelty: z.number().int().min(1).max(5).optional(), + reproducibility: z.number().int().min(1).max(5).optional(), + content: z.string().min(1).max(20000).optional(), + visibility: z.nativeEnum(ReviewVisibility).optional(), + status: z.nativeEnum(ReviewStatus).optional(), + }) + .refine((v) => Object.keys(v).length > 0, { message: "At least one field required" }); + +export async function GET(req: NextRequest, { params }: { params: { id: string } }) { + try { + const { userId } = await getAuth(); + const review = await getReview(prisma, params.id, userId); + if (!review) return NextResponse.json({ error: "Not found" }, { status: 404 }); + return NextResponse.json(review); + } catch (e) { + return handleError(e); + } +} + +export async function PATCH(req: NextRequest, { params }: { params: { id: string } }) { + try { + const { userId } = await requireAuth(); + const patch = PatchReviewSchema.parse(await req.json()); + const review = await updateReview(prisma, params.id, userId, patch); + return NextResponse.json(review); + } catch (e) { + return handleError(e); + } +} + +export async function DELETE(_req: NextRequest, { params }: { params: { id: string } }) { + try { + const { userId } = await requireAuth(); + const review = await prisma.review.findUnique({ + where: { id: params.id }, + select: { reviewerId: true, deletedAt: true }, + }); + if (!review || review.deletedAt) { + return NextResponse.json({ error: "Not found" }, { status: 404 }); + } + if (review.reviewerId !== userId) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + await prisma.review.update({ + where: { id: params.id }, + data: { deletedAt: new Date() }, + }); + return new NextResponse(null, { status: 204 }); + } catch (e) { + return handleError(e); + } +} diff --git a/community-reputation/src/app/api/reviews/route.ts b/community-reputation/src/app/api/reviews/route.ts new file mode 100644 index 0000000..957ab23 --- /dev/null +++ b/community-reputation/src/app/api/reviews/route.ts @@ -0,0 +1,51 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; +import { ReviewVisibility, ReviewStatus } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; +import { requireAuth, getAuth } from "@/lib/auth"; +import { handleError } from "@/lib/api"; +import { createReview, listReviews } from "@/services/review.service"; + +const CreateReviewSchema = z.object({ + projectId: z.string().min(1), + templateId: z.string().cuid().optional(), + clarity: z.number().int().min(1).max(5).optional(), + rigor: z.number().int().min(1).max(5).optional(), + novelty: z.number().int().min(1).max(5).optional(), + reproducibility: z.number().int().min(1).max(5).optional(), + content: z.string().min(1).max(20000), + visibility: z.nativeEnum(ReviewVisibility).optional(), + status: z.nativeEnum(ReviewStatus).optional(), +}); + +export async function POST(req: NextRequest) { + try { + const { userId } = await requireAuth(); + const body = CreateReviewSchema.parse(await req.json()); + const review = await createReview(prisma, { ...body, reviewerId: userId }); + return NextResponse.json(review, { status: 201 }); + } catch (e) { + return handleError(e); + } +} + +const ListReviewsQuerySchema = z.object({ + projectId: z.string().optional(), + reviewerId: z.string().optional(), + status: z.nativeEnum(ReviewStatus).optional(), +}); + +export async function GET(req: NextRequest) { + try { + const { userId } = await getAuth(); + const { searchParams } = new URL(req.url); + const query = ListReviewsQuerySchema.safeParse(Object.fromEntries(searchParams)); + if (!query.success) { + return NextResponse.json({ error: "Invalid query params", details: query.error.errors }, { status: 422 }); + } + const reviews = await listReviews(prisma, query.data, userId); + return NextResponse.json(reviews); + } catch (e) { + return handleError(e); + } +} diff --git a/community-reputation/src/app/api/users/[userId]/badges/route.ts b/community-reputation/src/app/api/users/[userId]/badges/route.ts new file mode 100644 index 0000000..c79175c --- /dev/null +++ b/community-reputation/src/app/api/users/[userId]/badges/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import { handleError } from "@/lib/api"; + +// Returns all badges earned by a user, with badge metadata, ordered by award date. +export async function GET(_req: NextRequest, { params }: { params: { userId: string } }) { + try { + const user = await prisma.user.findUnique({ + where: { id: params.userId }, + select: { id: true }, + }); + if (!user) return NextResponse.json({ error: "User not found" }, { status: 404 }); + + const badges = await prisma.userBadge.findMany({ + where: { userId: params.userId }, + include: { + badge: { + select: { slug: true, name: true, description: true, threshold: true }, + }, + }, + orderBy: { awardedAt: "desc" }, + }); + + return NextResponse.json(badges); + } catch (e) { + return handleError(e); + } +} diff --git a/community-reputation/src/app/auth/signin/page.tsx b/community-reputation/src/app/auth/signin/page.tsx new file mode 100644 index 0000000..5e22bf3 --- /dev/null +++ b/community-reputation/src/app/auth/signin/page.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { signIn } from "next-auth/react"; +import { useSearchParams } from "next/navigation"; +import { Suspense } from "react"; + +const DEMO_USERS = [ + { email: "alice.chen@mit.edu", name: "Alice Chen", role: "Admin · MIT · Biology", admin: true }, + { email: "bob.mehta@stanford.edu", name: "Bob Mehta", role: "Stanford · Physics" }, + { email: "claire.dupont@oxford.ac.uk", name: "Claire Dupont", role: "Oxford · Social Sciences" }, + { email: "diana.osei@harvard.edu", name: "Diana Osei", role: "Harvard · Biology" }, +]; + +function SignInContent() { + const params = useSearchParams(); + const callbackUrl = params.get("callbackUrl") ?? "/"; + + const login = (email: string) => + signIn("demo", { email, callbackUrl }); + + return ( +
+

Sign in

+

Choose a demo account to explore the platform.

+ +
+ {DEMO_USERS.map((u) => ( + + ))} +
+ +

+ GitHub OAuth is configured separately for production. These accounts are for demo only. +

+
+ ); +} + +export default function SignInPage() { + return ( + + + + ); +} diff --git a/community-reputation/src/app/globals.css b/community-reputation/src/app/globals.css new file mode 100644 index 0000000..743571f --- /dev/null +++ b/community-reputation/src/app/globals.css @@ -0,0 +1,646 @@ +:root { + --bg: #0f1117; + --surface: #1a1d27; + --surface-2: #232636; + --border: #2e3347; + --text: #e8eaf0; + --text-muted: #7c8194; + --accent: #6c8ef5; + --accent-dim: rgba(108, 142, 245, 0.12); + --success: #4caf8a; + --warning: #e8a020; + --danger: #e05555; + --radius: 8px; + --radius-sm: 4px; +} + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: 15px; + -webkit-font-smoothing: antialiased; +} + +body { + background: var(--bg); + color: var(--text); + font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", sans-serif; + line-height: 1.6; + min-height: 100vh; +} + +a { + color: var(--accent); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +/* ── Nav ─────────────────────────────────────────────────────────────────── */ + +nav { + background: var(--surface); + border-bottom: 1px solid var(--border); + padding: 0 1.5rem; + display: flex; + align-items: center; + gap: 2rem; + height: 52px; +} + +nav .brand { + font-weight: 700; + font-size: 1rem; + color: var(--text); + letter-spacing: -0.01em; +} + +nav .brand span { + color: var(--accent); +} + +nav .nav-links { + display: flex; + gap: 1.5rem; + list-style: none; +} + +nav .nav-links a { + color: var(--text-muted); + font-size: 0.875rem; + font-weight: 500; + transition: color 0.15s; +} + +nav .nav-links a:hover { + color: var(--text); + text-decoration: none; +} + +/* ── Layout ──────────────────────────────────────────────────────────────── */ + +main { + max-width: 1100px; + margin: 0 auto; + padding: 2rem 1.5rem; +} + +.page-title { + font-size: 1.6rem; + font-weight: 700; + letter-spacing: -0.02em; + margin-bottom: 0.25rem; +} + +.page-subtitle { + color: var(--text-muted); + font-size: 0.9rem; + margin-bottom: 1.75rem; +} + +.section-heading { + font-size: 1rem; + font-weight: 600; + letter-spacing: -0.01em; + margin-bottom: 0.875rem; + color: var(--text); +} + +.divider { + height: 1px; + background: var(--border); + margin: 1.5rem 0; +} + +/* ── Cards ───────────────────────────────────────────────────────────────── */ + +.card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.25rem; +} + +.card + .card { + margin-top: 0.75rem; +} + +/* ── Grid ────────────────────────────────────────────────────────────────── */ + +.grid-2 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +@media (max-width: 720px) { + .grid-2 { grid-template-columns: 1fr; } +} + +/* ── Profile header ──────────────────────────────────────────────────────── */ + +.profile-header { + display: flex; + align-items: flex-start; + gap: 1.5rem; + margin-bottom: 1.75rem; +} + +.avatar { + width: 72px; + height: 72px; + border-radius: 50%; + background: var(--surface-2); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.75rem; + font-weight: 700; + color: var(--accent); + flex-shrink: 0; + overflow: hidden; +} + +.avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.profile-meta h1 { + font-size: 1.5rem; + font-weight: 700; + letter-spacing: -0.02em; +} + +.profile-meta .meta-line { + color: var(--text-muted); + font-size: 0.875rem; + margin-top: 0.2rem; +} + +/* ── Reputation badge ────────────────────────────────────────────────────── */ + +.rep-score { + display: inline-flex; + align-items: center; + gap: 0.5rem; + background: var(--accent-dim); + border: 1px solid var(--accent); + border-radius: 9999px; + padding: 0.2rem 0.75rem; + font-size: 0.875rem; + font-weight: 700; + color: var(--accent); +} + +/* ── Breakdown bars ──────────────────────────────────────────────────────── */ + +.breakdown-list { + list-style: none; + display: flex; + flex-direction: column; + gap: 0.6rem; +} + +.breakdown-item { + display: grid; + grid-template-columns: 10rem 1fr 4rem; + align-items: center; + gap: 0.75rem; + font-size: 0.8rem; +} + +.breakdown-item .label { + color: var(--text-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.bar-track { + height: 6px; + background: var(--surface-2); + border-radius: 9999px; + overflow: hidden; +} + +.bar-fill { + height: 100%; + background: var(--accent); + border-radius: 9999px; + transition: width 0.3s; +} + +.breakdown-item .value { + text-align: right; + color: var(--text-muted); + font-variant-numeric: tabular-nums; +} + +/* ── Badge chips ─────────────────────────────────────────────────────────── */ + +.badge-chip { + display: inline-flex; + align-items: center; + gap: 0.35rem; + background: var(--surface-2); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 0.2rem 0.6rem; + font-size: 0.78rem; + font-weight: 500; + color: var(--warning); + cursor: default; +} + +.badge-chip .icon { + font-style: normal; +} + +/* ── CRediT role tags ─────────────────────────────────────────────────────── */ + +.credit-tag { + display: inline-block; + border-radius: var(--radius-sm); + padding: 0.15rem 0.5rem; + font-size: 0.75rem; + font-weight: 500; + background: var(--surface-2); + color: var(--accent); + border: 1px solid rgba(108, 142, 245, 0.25); +} + +/* ── Star rating ─────────────────────────────────────────────────────────── */ + +.stars { + display: inline-flex; + gap: 1px; + font-size: 0.9rem; +} + +.star-filled { + color: var(--warning); +} + +.star-empty { + color: var(--border); +} + +/* ── Review card ─────────────────────────────────────────────────────────── */ + +.review-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1rem 1.25rem; +} + +.review-card + .review-card { + margin-top: 0.75rem; +} + +.review-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-bottom: 0.75rem; +} + +.review-reviewer { + font-size: 0.875rem; + font-weight: 600; +} + +.review-scores { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-top: 0.75rem; + font-size: 0.78rem; + color: var(--text-muted); +} + +.review-score-item { + display: flex; + flex-direction: column; + gap: 0.1rem; +} + +.review-content { + font-size: 0.875rem; + line-height: 1.65; + color: var(--text); + margin-top: 0.5rem; +} + +/* ── Visibility pill ─────────────────────────────────────────────────────── */ + +.visibility-pill { + display: inline-block; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + padding: 0.1rem 0.45rem; + border-radius: 9999px; + background: var(--surface-2); + color: var(--text-muted); +} + +/* ── Leaderboard table ───────────────────────────────────────────────────── */ + +.leaderboard-table { + width: 100%; + border-collapse: collapse; + font-size: 0.875rem; +} + +.leaderboard-table th { + text-align: left; + padding: 0.6rem 0.75rem; + border-bottom: 1px solid var(--border); + color: var(--text-muted); + font-weight: 500; + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.leaderboard-table td { + padding: 0.65rem 0.75rem; + border-bottom: 1px solid var(--border); + vertical-align: middle; +} + +.leaderboard-table tr:last-child td { + border-bottom: none; +} + +.leaderboard-table tr:hover td { + background: var(--surface-2); +} + +.rank-cell { + color: var(--text-muted); + font-variant-numeric: tabular-nums; + font-weight: 600; + width: 2.5rem; +} + +.rank-cell.top-3 { + color: var(--warning); +} + +.score-cell { + font-variant-numeric: tabular-nums; + font-weight: 700; + color: var(--accent); +} + +/* ── Filter bar ──────────────────────────────────────────────────────────── */ + +.filter-bar { + display: flex; + flex-wrap: wrap; + gap: 0.6rem; + margin-bottom: 1.25rem; + align-items: center; +} + +.filter-bar select, +.filter-bar input { + background: var(--surface); + border: 1px solid var(--border); + color: var(--text); + border-radius: var(--radius-sm); + padding: 0.35rem 0.65rem; + font-size: 0.85rem; + appearance: none; + -webkit-appearance: none; + cursor: pointer; +} + +.filter-bar select:focus, +.filter-bar input:focus { + outline: 2px solid var(--accent); + outline-offset: 1px; +} + +/* ── Contribution list ───────────────────────────────────────────────────── */ + +.contrib-list { + list-style: none; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.contrib-item { + display: flex; + align-items: flex-start; + gap: 0.75rem; + font-size: 0.875rem; +} + +.contrib-item .credit-tag { + flex-shrink: 0; + margin-top: 0.1rem; +} + +.contrib-item .project-link { + font-weight: 500; +} + +.contrib-item .description { + color: var(--text-muted); + font-size: 0.8rem; +} + +/* ── Endorsement groups ──────────────────────────────────────────────────── */ + +.endorsement-group { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.5rem 0; + border-bottom: 1px solid var(--border); + font-size: 0.875rem; +} + +.endorsement-group:last-child { + border-bottom: none; +} + +.endorsement-skill { + font-weight: 500; + flex: 1; +} + +.endorsement-count { + background: var(--accent-dim); + color: var(--accent); + border-radius: 9999px; + padding: 0.1rem 0.55rem; + font-size: 0.75rem; + font-weight: 700; +} + +/* ── Review form ─────────────────────────────────────────────────────────── */ + +.form { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.form-group label { + font-size: 0.8rem; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.form-group textarea, +.form-group select, +.form-group input { + background: var(--surface-2); + border: 1px solid var(--border); + color: var(--text); + border-radius: var(--radius-sm); + padding: 0.55rem 0.75rem; + font-size: 0.875rem; + font-family: inherit; + resize: vertical; + appearance: none; + -webkit-appearance: none; +} + +.form-group textarea:focus, +.form-group select:focus, +.form-group input:focus { + outline: 2px solid var(--accent); + outline-offset: 1px; + border-color: var(--accent); +} + +.score-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 0.75rem; +} + +@media (max-width: 600px) { + .score-grid { grid-template-columns: 1fr 1fr; } +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + padding: 0.5rem 1.1rem; + border-radius: var(--radius-sm); + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + border: none; + transition: opacity 0.15s; +} + +.btn:hover { + opacity: 0.85; +} + +.btn:disabled { + opacity: 0.45; + cursor: not-allowed; +} + +.btn-primary { + background: var(--accent); + color: #fff; +} + +.btn-secondary { + background: var(--surface-2); + color: var(--text); + border: 1px solid var(--border); +} + +/* ── Empty state ─────────────────────────────────────────────────────────── */ + +.empty-state { + text-align: center; + padding: 2.5rem 1rem; + color: var(--text-muted); + font-size: 0.875rem; +} + +/* ── Alert ───────────────────────────────────────────────────────────────── */ + +.alert { + border-radius: var(--radius-sm); + padding: 0.6rem 0.9rem; + font-size: 0.85rem; +} + +.alert-success { background: rgba(76, 175, 138, 0.12); color: var(--success); border: 1px solid var(--success); } +.alert-error { background: rgba(224, 85, 85, 0.12); color: var(--danger); border: 1px solid var(--danger); } + +/* ── Tag pill ─────────────────────────────────────────────────────────────── */ + +.tag-pill { + display: inline-block; + background: var(--surface-2); + border: 1px solid var(--border); + border-radius: 9999px; + padding: 0.15rem 0.6rem; + font-size: 0.75rem; + color: var(--text-muted); +} + +/* ── Contributor graph card ──────────────────────────────────────────────── */ + +.contrib-graph { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.contrib-person { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.contrib-person-header { + display: flex; + align-items: center; + gap: 0.6rem; + font-size: 0.875rem; + font-weight: 500; +} + +.contrib-person-roles { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + padding-left: 0.25rem; +} diff --git a/community-reputation/src/app/layout.tsx b/community-reputation/src/app/layout.tsx new file mode 100644 index 0000000..4fcfd99 --- /dev/null +++ b/community-reputation/src/app/layout.tsx @@ -0,0 +1,26 @@ +import "./globals.css"; +import { Providers } from "@/components/Providers"; +import { DemoLoginBar } from "@/components/DemoLoginBar"; + +export const metadata = { title: "SCIBASE Community" }; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + + +
{children}
+
+ + + ); +} diff --git a/community-reputation/src/app/leaderboard/page.tsx b/community-reputation/src/app/leaderboard/page.tsx new file mode 100644 index 0000000..2197657 --- /dev/null +++ b/community-reputation/src/app/leaderboard/page.tsx @@ -0,0 +1,96 @@ +import { Suspense } from "react"; +import { prisma } from "@/lib/prisma"; +import { getLeaderboard } from "@/services/reputation.service"; +import { ReputationBadge } from "@/components/ReputationBadge"; +import { LeaderboardFilters } from "@/components/LeaderboardFilters"; + +interface PageProps { + searchParams: { domain?: string; region?: string; institution?: string; limit?: string }; +} + +async function LeaderboardTable({ searchParams }: PageProps) { + const limit = Math.min(parseInt(searchParams.limit ?? "50", 10) || 50, 100); + const entries = await getLeaderboard( + prisma, + { + domain: searchParams.domain || undefined, + region: searchParams.region || undefined, + institution: searchParams.institution || undefined, + }, + limit + ); + + if (entries.length === 0) { + return ( +
+ No users match these filters — or no reputation snapshots have been computed yet. +
+ + Run ./dev.sh --demo to seed sample data. + +
+ ); + } + + return ( +
+ + + + + + + + + + + + {entries.map((entry, i) => ( + + + + + + + + ))} + +
#ResearcherInstitutionDomainScore
{i + 1} + + {entry.name ?? "Anonymous"} + + + {entry.institution ?? "—"} + + {entry.domain ?? "—"} + + +
+
+ ); +} + +export default function LeaderboardPage({ searchParams }: PageProps) { + const hasFilter = searchParams.domain || searchParams.region || searchParams.institution; + return ( + <> +

Leaderboard

+

+ Researchers ranked by reputation score + {hasFilter ? " — filtered view" : ""} +

+ + + + + + Loading… + } + > + + + + ); +} diff --git a/community-reputation/src/app/page.tsx b/community-reputation/src/app/page.tsx new file mode 100644 index 0000000..5f5954c --- /dev/null +++ b/community-reputation/src/app/page.tsx @@ -0,0 +1,39 @@ +export default function Home() { + return ( + <> +

SCIBASE Community & Reputation

+

+ Transparent credit, peer review, and reputation for open science. +

+ +
+ +
🏆
+

Leaderboard

+

+ Researchers ranked by reputation score. Filter by domain, region, or institution. +

+
+ +
+
📡
+

REST API

+

+ All data is available via the JSON API. See{" "} + /api/leaderboards,{" "} + /api/reviews, and more. +

+
+
+ +
+ +

Quick links (demo data)

+ + + ); +} diff --git a/community-reputation/src/app/projects/[projectId]/page.tsx b/community-reputation/src/app/projects/[projectId]/page.tsx new file mode 100644 index 0000000..7aa23fc --- /dev/null +++ b/community-reputation/src/app/projects/[projectId]/page.tsx @@ -0,0 +1,206 @@ +import { notFound } from "next/navigation"; +import { ReviewStatus } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; +import { getAuth } from "@/lib/auth"; +import { serializeReview } from "@/lib/visibility"; +import { CreditRoleTag } from "@/components/CreditRoleTag"; +import { StarRating } from "@/components/StarRating"; +import { ReviewForm } from "@/components/ReviewForm"; +import { CommentPanel } from "@/components/CommentPanel"; + +interface PageProps { + params: { projectId: string }; +} + +export async function generateMetadata({ params }: PageProps) { + const project = await prisma.project.findUnique({ + where: { id: params.projectId }, + select: { title: true }, + }); + return { title: project?.title ? `${project.title} — SCIBASE` : "Project — SCIBASE" }; +} + +const VISIBILITY_LABELS: Record = { + PUBLIC: "Public", + SEMI_PRIVATE: "Semi-private", + ANONYMOUS: "Anonymous", + DOUBLE_BLIND: "Double-blind", +}; + +export default async function ProjectPage({ params }: PageProps) { + const { projectId } = params; + const { userId: viewerId } = await getAuth(); + + const [project, contributions, reviews] = await Promise.all([ + prisma.project.findUnique({ + where: { id: projectId }, + include: { owner: { select: { id: true, name: true, institution: true } } }, + }), + prisma.contribution.findMany({ + where: { projectId, deletedAt: null }, + include: { user: { select: { id: true, name: true } } }, + orderBy: { occurredAt: "asc" }, + }), + prisma.review.findMany({ + where: { + projectId, + deletedAt: null, + OR: [ + { status: ReviewStatus.PUBLISHED }, + // Show the authenticated user's own drafts + ...(viewerId ? [{ status: ReviewStatus.DRAFT, reviewerId: viewerId }] : []), + ], + }, + include: { + reviewer: { select: { id: true, name: true, institution: true } }, + }, + orderBy: { createdAt: "desc" }, + }), + ]); + + if (!project) notFound(); + + // Gather project author IDs for visibility enforcement + const authorIds = Array.from(new Set([ + project.ownerId, + ...contributions.map((c) => c.userId), + ])); + + const serializedReviews = reviews.map((r) => + serializeReview( + { + ...r, + reviewer: { id: r.reviewer.id, name: r.reviewer.name, institution: r.reviewer.institution }, + }, + viewerId, + authorIds + ) + ); + + // Group contributions by contributor + const contributorMap = new Map(); + for (const c of contributions) { + if (!contributorMap.has(c.userId)) { + contributorMap.set(c.userId, { name: c.user.name ?? "Anonymous", roles: [] }); + } + contributorMap.get(c.userId)!.roles.push(c.role); + } + + return ( + <> + {/* Project header */} +
+

{project.title}

+

+ Led by{" "} + {project.owner.name ?? "Anonymous"} + {project.owner.institution && ` · ${project.owner.institution}`} +

+
+ +
+ {/* Left — contributors + review form */} +
+ {/* CRediT contributor graph */} +
+

Contributors (CRediT)

+ {contributorMap.size === 0 ? ( +

+ No contributors recorded. +

+ ) : ( +
+ {Array.from(contributorMap.entries()).map(([uid, { name, roles }]) => ( +
+
+ {name} +
+
+ {roles.map((role) => ( + + ))} +
+
+ ))} +
+ )} +
+ + {/* Submit review */} +
+

Submit a Review

+ +
+
+ + {/* Right — peer reviews */} +
+

+ Peer Reviews{" "} + + ({serializedReviews.length}) + +

+ + {serializedReviews.length === 0 ? ( +
No published reviews yet.
+ ) : ( + serializedReviews.map((r) => ( +
+
+ + {r.reviewer?.name ?? "Anonymous reviewer"} + {r.reviewer?.institution && ( + + {" "}· {r.reviewer.institution} + + )} + +
+ {r.status === "DRAFT" && ( + Draft + )} + + {VISIBILITY_LABELS[r.visibility] ?? r.visibility} + +
+
+ + {(r.clarity || r.rigor || r.novelty || r.reproducibility) && ( +
+ {[ + ["Clarity", r.clarity], + ["Rigor", r.rigor], + ["Novelty", r.novelty], + ["Reproducibility", r.reproducibility], + ] + .filter(([, v]) => v !== null) + .map(([label, v]) => ( +
+ {label as string} + +
+ ))} +
+ )} + +

{r.content}

+ +
+

+ {new Date(r.createdAt).toLocaleDateString("en-GB", { + day: "numeric", + month: "short", + year: "numeric", + })} +

+
+ +
+ )) + )} +
+
+ + ); +} diff --git a/community-reputation/src/app/users/[userId]/page.tsx b/community-reputation/src/app/users/[userId]/page.tsx new file mode 100644 index 0000000..b0da0bb --- /dev/null +++ b/community-reputation/src/app/users/[userId]/page.tsx @@ -0,0 +1,196 @@ +import { notFound } from "next/navigation"; +import { prisma } from "@/lib/prisma"; +import { gatherComponents } from "@/services/reputation.service"; +import { computeReputation } from "@/lib/reputation"; +import { ReputationBadge } from "@/components/ReputationBadge"; +import { BadgeChip } from "@/components/BadgeChip"; +import { CreditRoleTag } from "@/components/CreditRoleTag"; +import { EndorseButton } from "@/components/EndorseButton"; + +interface PageProps { + params: { userId: string }; +} + +export async function generateMetadata({ params }: PageProps) { + const user = await prisma.user.findUnique({ + where: { id: params.userId }, + select: { name: true }, + }); + return { title: user?.name ? `${user.name} — SCIBASE` : "Researcher — SCIBASE" }; +} + +export default async function UserProfilePage({ params }: PageProps) { + const { userId } = params; + + const [user, latestSnapshot, badges, contributions, endorsements] = await Promise.all([ + prisma.user.findUnique({ + where: { id: userId }, + select: { id: true, name: true, email: true, image: true, institution: true, domain: true, region: true, createdAt: true }, + }), + prisma.reputationSnapshot.findFirst({ + where: { userId }, + orderBy: { computedAt: "desc" }, + }), + prisma.userBadge.findMany({ + where: { userId }, + include: { badge: { select: { slug: true, name: true, description: true } } }, + orderBy: { awardedAt: "desc" }, + }), + prisma.contribution.findMany({ + where: { userId, deletedAt: null }, + include: { project: { select: { id: true, title: true } } }, + orderBy: { occurredAt: "desc" }, + }), + prisma.endorsement.findMany({ + where: { endorseeId: userId, deletedAt: null }, + select: { skill: true, note: true, endorser: { select: { id: true, name: true } } }, + orderBy: { createdAt: "desc" }, + }), + ]); + + if (!user) notFound(); + + // Compute live reputation if no snapshot exists yet + let total = latestSnapshot?.total ?? 0; + let breakdown = latestSnapshot?.breakdown as { component: string; rawValue: number; smoothed: number; weighted: number }[] | null; + + if (!latestSnapshot) { + const components = await gatherComponents(prisma, userId); + const result = computeReputation(components); + total = result.total; + breakdown = result.breakdown; + } + + // Group endorsements by skill + const endorsementsBySkill = endorsements.reduce>( + (acc, e) => { + if (!acc[e.skill]) acc[e.skill] = { count: 0, endorsers: [] }; + acc[e.skill].count++; + if (e.endorser?.name) acc[e.skill].endorsers.push(e.endorser.name); + return acc; + }, + {} + ); + + const initials = (user.name ?? "?") + .split(" ") + .map((p) => p[0]) + .slice(0, 2) + .join("") + .toUpperCase(); + + return ( + <> + {/* Profile header */} +
+
+ {user.image ? ( + // eslint-disable-next-line @next/next/no-img-element + {user.name + ) : ( + initials + )} +
+
+

{user.name ?? "Anonymous Researcher"}

+

+ {[user.institution, user.domain, user.region].filter(Boolean).join(" · ")} +

+

+ +

+
+
+ +
+
+ +
+ {/* Left column */} +
+ {/* Reputation breakdown */} +
+

Reputation

+ [0]["breakdown"]} + /> +
+ + {/* Badges */} +
+

Badges

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

No badges yet.

+ ) : ( +
+ {badges.map((ub) => ( + + ))} +
+ )} +
+
+ + {/* Right column */} +
+ {/* Endorsements */} +
+

Endorsements

+ {Object.keys(endorsementsBySkill).length === 0 ? ( +

No endorsements yet.

+ ) : ( +
+ {Object.entries(endorsementsBySkill) + .sort((a, b) => b[1].count - a[1].count) + .map(([skill, { count, endorsers }]) => ( +
+ {skill} + + {count} + +
+ ))} +
+ )} +
+ + {/* CRediT contributions */} +
+
+

Contributions

+ + Export CSV + +
+ {contributions.length === 0 ? ( +

No contributions recorded.

+ ) : ( +
    + {contributions.map((c) => ( +
  • + +
    + + {c.project.title} + + {c.description && ( +

    {c.description}

    + )} +
    +
  • + ))} +
+ )} +
+
+
+ + ); +} diff --git a/community-reputation/src/components/BadgeChip.tsx b/community-reputation/src/components/BadgeChip.tsx new file mode 100644 index 0000000..2c264b0 --- /dev/null +++ b/community-reputation/src/components/BadgeChip.tsx @@ -0,0 +1,31 @@ +interface BadgeData { + slug: string; + name: string; + description: string; +} + +interface Props { + badge: BadgeData; +} + +const BADGE_ICONS: Record = { + "first-review": "✍", + "prolific-reviewer": "📝", + "trusted-reviewer": "⭐", + "peer-review-legend": "🏆", + "first-endorsement": "👍", + "well-endorsed": "🎖", + "first-reproduction": "🔬", + "reproducibility-champion": "🛡", + default: "◉", +}; + +export function BadgeChip({ badge }: Props) { + const icon = BADGE_ICONS[badge.slug] ?? BADGE_ICONS.default; + return ( + + {icon} + {badge.name} + + ); +} diff --git a/community-reputation/src/components/CommentPanel.tsx b/community-reputation/src/components/CommentPanel.tsx new file mode 100644 index 0000000..f6d88b5 --- /dev/null +++ b/community-reputation/src/components/CommentPanel.tsx @@ -0,0 +1,279 @@ +"use client"; + +import { useSession } from "next-auth/react"; +import { useCallback, useEffect, useState } from "react"; + +interface CommentAuthor { + id: string; + name: string | null; +} + +interface Comment { + id: string; + body: string; + authorId: string; + author: CommentAuthor; + parentId: string | null; + createdAt: string; + replies?: Comment[]; +} + +interface Props { + targetId: string; + targetType?: string; + initialCount?: number; +} + +function timeAgo(dateStr: string) { + const diff = Date.now() - new Date(dateStr).getTime(); + const mins = Math.floor(diff / 60000); + if (mins < 1) return "just now"; + if (mins < 60) return `${mins}m ago`; + const hrs = Math.floor(mins / 60); + if (hrs < 24) return `${hrs}h ago`; + return `${Math.floor(hrs / 24)}d ago`; +} + +function CommentItem({ + comment, + onReply, +}: { + comment: Comment; + onReply: (parentId: string, body: string) => Promise; +}) { + const { data: session } = useSession(); + const [replyOpen, setReplyOpen] = useState(false); + const [replyBody, setReplyBody] = useState(""); + const [saving, setSaving] = useState(false); + + const submitReply = async () => { + if (!replyBody.trim()) return; + setSaving(true); + await onReply(comment.id, replyBody.trim()); + setReplyBody(""); + setReplyOpen(false); + setSaving(false); + }; + + return ( +
+
+
+ {(comment.author.name ?? "?")[0].toUpperCase()} +
+
+
+ + {comment.author.name ?? "Anonymous"} + + + {timeAgo(comment.createdAt)} + +
+

{comment.body}

+ {session && ( + + )} + {replyOpen && ( +
+