diff --git a/.agents/README.md b/.agents/README.md deleted file mode 100644 index 8731bf1..0000000 --- a/.agents/README.md +++ /dev/null @@ -1,139 +0,0 @@ -# MetaBuff Agent System - -This directory contains the **MetaBuff agent orchestration system** — a set of AI coding agents that work together to decompose, implement, validate, and test complex coding tasks. - -## Architecture Overview - -``` - ┌─────────────┐ - │ metabuff │ ← Main orchestrator (complexity router) - └──────┬──────┘ - │ - ┌─────────────┼─────────────┐ - │ │ │ - Simple Complex Mega - (1-2 files) (multi-file) (parallel) - │ │ │ - base(CoT v2) planner|reasoner metabuff-mega - │ (CoT v2) (cascade waves) - typecheck reviewer │ - [regex-guard] typecheck file-picker - validator regex-guard thinker → N subtasks - validator wave1[≤6] → inter-review - wave2[≤6] → ... - synthesis-review - regex-guard - typecheck+tests - validator -``` - -### Agent Roles - -| Agent | Version | Role | Description | -|-------|---------|------|-------------| -| `metabuff` | v1.4.0 | **Orchestrator** | Classifies task complexity (simple/complex/mega) and routes to the optimal pipeline. Enforces CoT v2 with Socratic pre-flight. Routes algorithm tasks to `metabuff-reasoner`. | -| `metabuff-mega` | v1.2.0 | **Parallel Spawner** | Decomposes large tasks into up to 12 subtasks, runs them in cascade waves of ≤6 parallel agents (Antigravity 2.0 pattern, Freebuff-safe). Supports dynamic `custom` specialist types. | -| `metabuff-reasoner` | v1.0.0 | **Deep Logic Specialist** | 6-step Socratic protocol for algorithmic tasks (understand → challenge → explore → select → implement → prove). Uses `effort: 'high'`. Closes single-model reasoning gap (~55 → ~72/100 vs Claude Opus). | -| `metabuff-regex-guard` | v1.0.0 | **Regex Safety Validator** | Catches runtime-invalid regex patterns TypeScript's type checker misses. 4-phase scan: syntax, ReDoS, double-escape, empty alternation. Spawned after any AI-generated code. | -| `metabuff-validator` | v1.1.0 | **Validator** | Post-execution audit: ghost imports, phantom edits, broken tests, incomplete TODOs, regex safety delegation, and function signature consistency. | -| `metabuff-testgen` | v1.0.0 | **Test Generator** | Writes unit/integration tests matching the project's existing style. | -| `metabuff-arch` | v1.0.0 | **Architecture Analyst** | Handles data model design, API contracts, component structure, and dependency analysis. | -| `metabuff-security` | v1.0.0 | **Security Analyst** | Audits for hardcoded secrets, injection vulnerabilities, auth gaps, and insecure patterns. | - -## What Closed in v1.4.0 - -| Gap (from v1.3.0 chart) | Fix | Expected Impact | -|------------------------|-----|-----------------| -| Single-model reasoning: ~55/100 | `metabuff-reasoner`: 6-step Socratic + effort=high routed by `isAlgorithmTask` | ~72/100 | -| Hallucination control: ~65/100 | CoT v2 STEP 2.5 "3 failure modes before coding" | ~72/100 | -| Parallel scale: 6 agents hard ceiling | Cascade waves: 2× waves of 6 = 12 effective specialists | 12 agents total | -| Dynamic agent creation: Static pool | `custom` specialist type in mega thinker schema with `customRole`+`customSystemAddition` | Near-dynamic | -| Regex runtime errors: undetected | `metabuff-regex-guard` in all pipelines; `metabuff-validator` v1.1.0 auto-delegates | Eliminated class | - -## Agent Definition Structure - -Every agent file exports a default `AgentDefinition` object: - -```typescript -import { AgentDefinition } from './types/agent-definition' - -const definition: AgentDefinition = { - id: 'my-agent', - version: '1.0.0', - displayName: 'My Agent', - spawnerPrompt: 'Short description for spawning this agent.', - systemPrompt: 'System prompt that sets the agent\'s personality and rules.', - model: 'deepseek/deepseek-v4-flash', - reasoningOptions: { enabled: true, exclude: false, effort: 'medium' }, - toolNames: ['read_files', 'str_replace', 'write_file', 'spawn_agents', 'end_turn'], - spawnableAgents: ['codebuff/base@0.0.1'], - handleSteps: function* ({ prompt }) { - // Generator-based execution flow - }, -} - -export default definition -``` - -## ⚠️ CRITICAL RULE: Inline Helpers Inside `handleSteps` - -**Never reference module-level functions or constants inside `handleSteps`.** - -The agent execution framework extracts only the exported `definition` object — module-level function bindings are NOT preserved. Inline ALL helpers inside `handleSteps`. - -```typescript -// ❌ WRONG — lost at runtime -function helper() { return 'value' } -const definition = { handleSteps: function* () { helper() } } - -// ✅ CORRECT — inlined inside the generator -const definition = { - handleSteps: function* () { - function helper() { return 'value' } // Safe: inside the closure - helper() - }, -} -``` - -**Exception:** Module-level constants used only in definition *properties* (not inside `handleSteps`) are safe — they're evaluated at import time. - -### Files that follow this rule - -| File | Has `handleSteps` | Status | -|------|:---:|:---:| -| `metabuff.ts` | ✅ | ✅ Inlined | -| `metabuff-mega.ts` | ✅ | ✅ Inlined | -| `metabuff-validator.ts` | ❌ | ✅ N/A | -| `metabuff-reasoner.ts` | ❌ | ✅ N/A | -| `metabuff-regex-guard.ts` | ❌ | ✅ N/A — REGEX_SCAN_COMMAND is a module-level const used only in instructionsPrompt (safe) | -| `metabuff-testgen.ts` | ❌ | ✅ N/A | -| `metabuff-arch.ts` | ❌ | ✅ N/A | -| `metabuff-security.ts` | ❌ | ✅ N/A | - -## Anti-Hallucination Protocol (CoT v2) - -All MetaBuff implementation agents use CoT v2, which adds a mandatory **Socratic pre-flight** step: - -1. **ORIENT** — State the goal and list all files to read -2. **GROUND** — Read files and verify all symbols before referencing them -3. **QUESTION** *(NEW v1.4.0)* — Articulate 3 failure modes; flag assumptions; resolve before continuing -4. **PLAN** — Numbered action plan; flag `⚠ UNCERTAIN` items -5. **EXECUTE** — Targeted edits with narration -6. **VERIFY** — Re-read, run tests, fix issues - -### Grounding Rules (Never Violate) - -- ✗ Do not reference a file path without having read it -- ✗ Do not assume a function/type exists — verify with `code_searcher` -- ✗ Do not invent package names or import paths -- ✗ Do not leave TODOs or placeholder code -- ✗ Do not proceed with unresolved `⚠ UNCERTAIN` items - -## Performance Constraints - -| Constraint | Value | Reason | -|-----------|-------|--------| -| `MAX_WAVE_SIZE` | 6 | Hard limit — more concurrent spawns freeze/crash Freebuff | -| `MAX_DECOMP_TASKS` | 12 | Soft limit — 2 waves of 6 = 12 effective specialists | -| `BASHER_TIMEOUT` | 60s (simple/complex), 120s (mega) | Prevent infinite hangs | -| Reasoner effort | `'high'` | Only for algorithm tasks — avoids unnecessary cost on standard tasks | diff --git a/.agents/known-issues.md b/.agents/known-issues.md deleted file mode 100644 index 1271cee..0000000 --- a/.agents/known-issues.md +++ /dev/null @@ -1,37 +0,0 @@ -# MetaBuff Known Issues & Lessons Learned - -This file persists across sessions to provide inter-session memory. -Auto-populated by MetaBuff pipelines. Do not manually edit unless reviewing accuracy. - -## Format -- `[DATE] CATEGORY: Issue description → Resolution/Fix` -- Categories: `HALLUCINATION | TYPE_ERROR | TEST_FAILURE | RUNTIME_ERROR | DESIGN_ISSUE | PERFORMANCE` - -## Entries - - -- `[2026-05-30] HALLUCINATION: Module-level functions in handleSteps are not preserved at runtime because the agent execution framework extracts only the exported definition object. Inlining all helpers inside handleSteps fixed this.` -- `[2026-05-30] DESIGN_ISSUE: Complexity analysis had no upper bound — mentioning "refactor" 5 times scored 10+, causing false mega classification. Added COMPLEXITY_SATURATION = 8 and diminishing returns on keyword scoring.` -- `[2026-05-30] DESIGN_ISSUE: Parallel agent execution could produce conflicting edits in the same file. Added post-mega continuous validation checkpoint to detect and fix merge conflicts.` -- `[2026-05-30] DESIGN_ISSUE: No inter-session memory — each session started fresh, repeating past mistakes. Added known-issues.md with CoT prompt instructions for all agents to read it before starting work.` -- `[2026-05-30] RUNTIME_ERROR: Basher commands had no timeout — long-running commands could hang indefinitely. Added BASHER_TIMEOUT = 60s and explicit timeout_seconds on all basher spawns.` -- `[2026-05-30] TYPE_ERROR: Prisma client types (@prisma/client) fail to generate when schema has migration issues. Pre-existing project issue — run npx prisma generate to regenerate types after schema changes.` -- `[2026-05-30] PERFORMANCE: Full typecheck on every pipeline run (60+ files) was slow for simple 1-2 file changes. Simple pipeline typechecks only edited files via head -40.` -- `[2026-05-30] HALLUCINATION: Agents would write new files but never verify they compile. Added sandbox compile check (git diff --diff-filter=A) after generation tasks.` -- `[2026-05-30] DESIGN_ISSUE: Debugging pipeline failures was hard without intermediate checkpoints. Added continuous validation checkpoints after each major pipeline phase.` -- `[2026-05-31] PERFORMANCE: Simple tier ran 4-5 sequential spawns causing Freebuff freeze on trivial tasks. Reduced to base + targeted typecheck + conditional regex-guard + conditional sandbox + validator.` -- `[2026-05-31] PERFORMANCE: Typecheck in simple tier ran full bun run typecheck (60+ file scan). Fixed to targeted check on git diff --name-only changed .ts/.tsx files only.` -- `[2026-05-31] BUG: MAX_PIPELINE_RUNS=3, MAX_MEMORY_ENTRIES=5, MAX_MEMORY_FILE_LINES=60 were declared inside handleSteps but never referenced. Removed to prevent confusion.` -- `[2026-05-31] BUG: metabuff-mega.ts used const { toolResult } = yield {...} to capture thinker output — framework does not return values from yield. Fixed by using think_deeply to extract JSON from message history after thinker runs.` -- `[2026-05-31] BUG: SECURITY_RED_FLAGS had 14 patterns but instructionsPrompt used .slice(0,6), silently skipping 8 patterns. Fixed to use all patterns.` -- `[2026-05-31] BUG: withCoT was applied to reviewer and validator agents. The EXECUTE step confused them into re-implementing rather than reviewing. Added lighter withReview() variant for read-only agents.` -- `[2026-05-31] BUG: isGenerationTask regex matched "write a comment" and "create a variable". Tightened to explicit new-file patterns only.` -- `[2026-05-31] DESIGN_ISSUE: Inter-session memory was a footnote in withCoT — agents ignored it. Promoted to a mandatory first spawn (basher cat) before complexity analysis.` -- `[2026-05-31] QUAL: typecheck command had no fallback — if project lacks typecheck script in package.json, it silently failed. Added fallback: npx tsc --noEmit.` -- `[2026-05-31] BUG: Targeted typecheck in simple tier used git diff HEAD --name-only which only shows tracked changes. Brand-new untracked files created by the agent were silently skipped. Fixed to also check git ls-files --others --exclude-standard.` -- `[2026-05-31] QUAL: v1.4.0 — isAlgorithmTask routes algorithm/DP/parser tasks to metabuff-reasoner (effort=high, Socratic 6-step). Improves single-model reasoning score from ~55 to ~72/100.` -- `[2026-05-31] QUAL: v1.4.0 — CoT v2 adds STEP 2.5 QUESTION: agents must articulate 3 failure modes before coding. Reduces assumption-based hallucinations.` -- `[2026-05-31] SAFETY: v1.4.0 — metabuff-regex-guard added to all pipelines. TypeScript type checker silently accepts runtime-invalid regex. Guard catches: syntax errors, ReDoS nested quantifiers, double-escape mistakes, empty alternation.` -- `[2026-05-31] QUAL: v1.4.0 / v1.2.0 — metabuff-mega cascade wave pattern: MAX_DECOMP_TASKS raised to 12, split into waves of ≤6 (MAX_WAVE_SIZE). Achieves 12 effective specialists without exceeding Freebuff crash limit. Inter-wave reviews maintain coherence between waves.` -- `[2026-05-31] QUAL: v1.4.0 / v1.2.0 — metabuff-mega custom specialist type: thinker can now emit specialist: 'custom' with customRole and customSystemAddition fields, enabling dynamic agent creation without framework changes.` -- `[2026-06-01] QUAL: Added Google Gemini 3.1 Flash-Lite as primary model for Owel chatbot (both frontend LangChain agent and backend RAG pipeline). Frontend uses ChatGoogleGenerativeAI directly via GOOGLE_API_KEY with Groq Llama 3 fallback. Backend uses google/gemini-3.1-flash-lite via OpenRouter as first model in the 7-model fallback chain. Requires GOOGLE_API_KEY on Vercel (frontend); backend falls back gracefully if Gemini via OpenRouter is unavailable.` diff --git a/.agents/metabuff-arch.ts b/.agents/metabuff-arch.ts deleted file mode 100644 index f250e93..0000000 --- a/.agents/metabuff-arch.ts +++ /dev/null @@ -1,104 +0,0 @@ -/** - * MetaBuff Arch — Architecture Analyst Specialist - * ───────────────────────────────────────────────── - * Handles the architectural dimension of complex tasks: - * • System design and component decomposition - * • Data model design (schema, types, interfaces) - * • API contracts (REST, GraphQL, RPC definitions) - * • Dependency graphs and coupling analysis - * • Architectural decision records (ADRs) - * - * Spawned by metabuff-mega for the 'arch' subtask category. - * Can also be spawned directly for design-only tasks. - */ - -import { AgentDefinition } from './types/agent-definition' - -const FREE_MODEL = 'deepseek/deepseek-v4-pro' // Primary; falls back to deepseek-v4-flash when unavailable - -const definition: AgentDefinition = { - id: 'metabuff-arch', - version: '1.0.0', - displayName: 'MetaBuff Architecture Analyst', - - spawnerPrompt: - 'Spawn for architecture concerns: data model design, API contract definition, ' + - 'component structure, dependency analysis, or system-level design decisions.', - - model: FREE_MODEL, - - reasoningOptions: { - enabled: true, - exclude: false, - effort: 'high', // architecture needs the most careful reasoning - }, - - toolNames: [ - 'read_files', - 'code_searcher', - 'file_picker', - 'write_file', - 'str_replace', - 'basher', - 'spawn_agents', - 'think_deeply', - 'glob', - 'end_turn', - ], - - spawnableAgents: [ - 'codebuff/thinker@0.0.1', - 'codebuff/researcher@0.0.1', - ], - - systemPrompt: `You are MetaBuff's architecture specialist. -You think about systems before touching files. - -YOUR FOCUS: - • Data models: schemas, types, interfaces, DTOs, domain objects - • API contracts: request/response shapes, route definitions, error formats - • Component boundaries: what each module owns and doesn't own - • Dependency direction: nothing in the core should import from infra - • Extensibility: design for the next change, not just this one - -ARCHITECTURE PRINCIPLES (apply unless codebase already contradicts them): - • Single Responsibility: each file/module has one reason to change - • Dependency Inversion: depend on abstractions, not concretions - • Explicit over implicit: data shapes should be typed, not inferred as any - • Co-locate tests with the code they test - • Never use circular imports - -HALLUCINATION PREVENTION: - Read the existing architecture before proposing changes. - Grep for patterns already in use — don't introduce a third way of doing something.`, - - instructionsPrompt: ` -For your assigned architectural subtask: - -1. Read the codebase architecture first: - - Find and read existing type/interface/schema files - - Find existing API route definitions - - Check for any architecture.md, ADR folder, or design docs - -2. Identify what already exists that you can extend (don't duplicate) - -3. Design your changes: - - Define all new types/interfaces first (no any, no unknown unless justified) - - Specify API contracts as TypeScript types or OpenAPI-equivalent comments - - Draw the component boundary explicitly in your plan - -4. Implement: - - Create/update type files first - - Update interfaces before updating implementations - - Add JSDoc comments to all public types you create - -5. Verify consistency: - - code_searcher for every new type you defined to make sure it's used correctly - - Make sure no circular imports were introduced (check imports in changed files)`, - - stepPrompt: - 'Continue the architectural work. ' + - 'If you have completed the design and implementation, verify consistency and call end_turn.', -} - -export default definition diff --git a/.agents/metabuff-mega.ts b/.agents/metabuff-mega.ts deleted file mode 100644 index 88b689d..0000000 --- a/.agents/metabuff-mega.ts +++ /dev/null @@ -1,394 +0,0 @@ -/** - * MetaBuff Mega — Antigravity 2.0-Style Parallel Agent Spawner v1.2.0 - * ───────────────────────────────────────────────────────────────────── - * For large-scale tasks (full-system refactors, new features spanning many - * subsystems, or anything the complexity analyzer scored 6+). - * - * CHANGES FROM v1.1.0: - * • [QUAL] Cascade wave pattern: MAX_DECOMP_TASKS raised from 6 to 12. - * Subtasks are split into sequential waves of ≤ MAX_WAVE_SIZE (6). - * Closes scale gap vs Antigravity 2.0 — 12 effective specialist agents - * without exceeding the Freebuff 6-concurrent freeze limit. - * Between waves, a lightweight integration review ensures wave 2 agents - * have accurate context from wave 1 changes. - * • [QUAL] Dynamic specialist type: thinker can now output specialist: 'custom' - * with customRole + customSystemAddition fields to create purpose-built - * agents for novel task categories (e.g., 'i18n-specialist', 'migration-specialist'). - * Closes the static-pool gap vs Antigravity 2.0's fully dynamic agent creation. - * • [QUAL] Algorithm routing: 'reason' specialist type added → routes to - * metabuff-reasoner (effort=high, Socratic 6-step) for algorithmically complex - * subtasks. - * • [SAFETY] metabuff-regex-guard added after synthesis review. - * All generated code in mega tasks is now scanned for runtime-invalid regex. - * • [QUAL] Thinker prompt updated: documents new specialists + cascade awareness. - * • [QUAL] parseDecomposition handles new fields (customRole, customSystemAddition). - * - * FLOW: - * 1. File-picker maps relevant codebase structure - * 2. Thinker decomposes into 3–12 subtasks (JSON), including optional 'custom' types - * 3. think_deeply extracts the JSON from thinker's response - * 4. Subtasks split into waves of ≤ MAX_WAVE_SIZE (6) — Freebuff stability limit - * 5. Wave 1 runs in parallel; inter-wave review if Wave 2+ exists - * 6. Wave 2+ runs in parallel (building on wave 1 context) - * 7. Synthesis reviewer integrates all parallel outputs - * 8. Regex guard scans all generated code - * 9. Basher runs full typecheck + tests - * 10. Validator does final anti-hallucination pass - * - * PERFORMANCE CONSTRAINTS (NEVER VIOLATE): - * • MAX_WAVE_SIZE = 6 — hard limit for Freebuff concurrent spawn stability. - * More than 6 simultaneous spawns cause Freebuff to freeze or crash. - * • MAX_DECOMP_TASKS = 12 — thinker soft limit (2 waves of 6 max). - * Increase only if cascade waves are proven stable in your environment. - * - * CRITICAL NOTE: - * All helpers inlined inside handleSteps — module-level functions are NOT - * preserved by the agent execution framework (see README.md for details). - */ - -import { AgentDefinition } from './types/agent-definition' - -const definition: AgentDefinition = { - id: 'metabuff-mega', - version: '1.2.0', - displayName: 'MetaBuff Mega (Cascade Parallel Spawner)', - - spawnerPrompt: - 'Spawn for large-scale tasks: full-system refactors, new features spanning many files, ' + - 'architectural changes, or anything requiring more than 5 files to change. ' + - 'MetaBuff Mega decomposes the task into up to 12 subtasks and runs them in ' + - 'cascade waves of ≤6 parallel agents (Antigravity 2.0 pattern, Freebuff-safe).', - - model: 'deepseek/deepseek-v4-pro', // Primary; falls back to deepseek-v4-flash when unavailable - - reasoningOptions: { - enabled: true, - exclude: false, - effort: 'high', - }, - - toolNames: ['spawn_agents', 'think_deeply', 'end_turn'], - - spawnableAgents: [ - 'codebuff/base@0.0.1', - 'codebuff/thinker@0.0.1', - 'codebuff/reviewer@0.0.1', - 'codebuff/researcher@0.0.1', - 'codebuff/file-picker@0.0.1', - 'basher', - 'metabuff-arch', - 'metabuff-security', - 'metabuff-testgen', - 'metabuff-reasoner', // v1.2.0: algorithm specialist - 'metabuff-regex-guard', // v1.2.0: runtime regex safety - 'metabuff-validator', - ], - - systemPrompt: - 'You are the MetaBuff Mega orchestrator. ' + - 'You never write code directly. ' + - 'Your job is to decompose large tasks into parallel subtasks and coordinate specialist agents ' + - 'in cascade waves (never more than 6 concurrent spawns). ' + - 'Think of yourself as the Technical Director for a parallel coding team.', - - handleSteps: function* ({ prompt }) { - - /** - * Hard limit: concurrent spawns that won't freeze Freebuff. - * NEVER increase this without testing Freebuff stability. - */ - const MAX_WAVE_SIZE = 6 - - /** - * Soft limit: maximum subtasks the thinker can create. - * At 12 tasks and wave size 6 → 2 waves = 12 effective specialists total. - */ - const MAX_DECOMP_TASKS = 12 - - /** Timeout for typecheck/test basher commands */ - const BASHER_TIMEOUT = 120 - - // ─── HELPER: Resolve specialist tag → agent type string ─────────────────── - function resolveAgent(specialist: string): string { - const map: Record = { - arch: 'metabuff-arch', - security: 'metabuff-security', - testgen: 'metabuff-testgen', - base: 'codebuff/base@0.0.1', - research: 'codebuff/researcher@0.0.1', - review: 'codebuff/reviewer@0.0.1', - reason: 'metabuff-reasoner', // v1.2.0 - custom: 'codebuff/base@0.0.1', // v1.2.0: dynamic via custom prompt - } - return map[specialist] ?? 'codebuff/base@0.0.1' - } - - // ─── HELPER: Split array into waves ─────────────────────────────────────── - function splitIntoWaves(items: T[], waveSize: number): T[][] { - const waves: T[][] = [] - for (let i = 0; i < items.length; i += waveSize) { - waves.push(items.slice(i, i + waveSize)) - } - return waves - } - - // ─── HELPER: Parse decomposition ────────────────────────────────────────── - function parseDecomposition( - raw: string | undefined, - fallbackPrompt: string, - ): Array<{ - subtask: string - specialist: string - focus: string - customRole?: string - customSystemAddition?: string - }> { - if (!raw) return [{ - subtask: fallbackPrompt, - specialist: 'base', - focus: 'full implementation', - }] - - const jsonMatch = raw.match(/\[[\s\S]*?\]/s) - if (jsonMatch) { - try { - const parsed = JSON.parse(jsonMatch[0]) as unknown[] - if (Array.isArray(parsed) && parsed.length > 0) { - return (parsed as Array<{ - subtask: string - specialist: string - focus: string - customRole?: string - customSystemAddition?: string - }>) - .slice(0, MAX_DECOMP_TASKS) - .filter(s => typeof s.subtask === 'string' && typeof s.specialist === 'string') - } - } catch { - // fall through to bullet-list fallback - } - } - - const lines = raw.split('\n').filter(l => /^\s*[-\d*•]/.test(l)).slice(0, MAX_DECOMP_TASKS) - if (lines.length > 1) { - return lines.map((line, i) => ({ - subtask: line.replace(/^\s*[-\d.*•]+\s*/, ''), - specialist: i === 0 ? 'arch' : i === lines.length - 1 ? 'testgen' : 'base', - focus: `part ${i + 1} of ${lines.length}`, - })) - } - - return [{ subtask: fallbackPrompt, specialist: 'base', focus: 'full implementation' }] - } - - // ─── COT prefix for all specialist agents ───────────────────────────────── - const COT_SYSTEM_PREFIX = `You are a specialist agent in MetaBuff's parallel execution pipeline. -You are responsible for ONE specific subtask of a larger system. - -PROTOCOL: - 1. Read every file relevant to your subtask before touching anything - 2. Verify all symbols, imports, and types you plan to use via code_searcher - 3. Make your changes with surgical str_replace operations - 4. Leave a brief comment in each changed file: // [MetaBuff Mega: ] - 5. Do NOT attempt to handle subtasks assigned to other specialist agents - 6. Call end_turn only when your subtask is complete and verified - -ANTI-HALLUCINATION (non-negotiable): - ✗ Do not reference a file path without having read it this session - ✗ Do not assume a function or type exists — verify with code_searcher - ✗ Do not invent package names or import paths - ✗ Do not leave TODOs or placeholder code - ✗ Do not call end_turn if there are unresolved ⚠ UNCERTAIN items - -` - - // ── Phase 0: Codebase mapping ────────────────────────────────────────────── - yield { - toolName: 'spawn_agents', - input: { - agents: [{ - agent_type: 'codebuff/file-picker@0.0.1', - prompt: - `Map the entire codebase structure relevant to this task.\n` + - `List key files, their roles, and how they interconnect.\n` + - `Task: ${prompt}`, - }], - }, - } - - // ── Phase 1: Task decomposition ─────────────────────────────────────────── - yield { - toolName: 'spawn_agents', - input: { - agents: [{ - agent_type: 'codebuff/thinker@0.0.1', - prompt: - `You are decomposing a large coding task for parallel cascade execution.\n\n` + - `Task: ${prompt}\n\n` + - `Output ONLY a JSON array (no markdown, no explanation) of 3–${MAX_DECOMP_TASKS} subtasks.\n` + - `Each element:\n` + - ` {\n` + - ` "subtask": "Detailed description of what to implement",\n` + - ` "specialist": "arch|security|testgen|base|research|reason|custom",\n` + - ` "focus": "one-line display name",\n` + - ` "customRole": "(only when specialist=custom) short role title",\n` + - ` "customSystemAddition": "(only when specialist=custom) extra system prompt text"\n` + - ` }\n\n` + - `Specialist guide:\n` + - ` arch → system design, component structure, data models, API contracts\n` + - ` security → auth flows, input validation, secrets, access control\n` + - ` testgen → unit + integration tests for changed code\n` + - ` base → general implementation: business logic, UI, utilities\n` + - ` research → documentation, README, changelog, ADRs\n` + - ` reason → algorithm design, state machines, performance optimization,\n` + - ` parsers, numerical computation — anything requiring proof-level thinking\n` + - ` custom → novel task-specific roles not covered above.\n` + - ` Set customRole = "short-role-name" (e.g., "migration-specialist")\n` + - ` Set customSystemAddition = "You focus exclusively on [specific aspect]..."\n\n` + - `Cascade wave rules:\n` + - ` - Subtasks run in waves of ${MAX_WAVE_SIZE} concurrent agents\n` + - ` - Order subtasks so FOUNDATION work (arch, data models) appears first\n` + - ` - Implementation subtasks that depend on the schema come in wave 2\n` + - ` - testgen + research always go in the LAST wave\n` + - ` - Always include at least one testgen subtask\n` + - ` - Always include arch if the task touches data models or APIs\n` + - ` - Each file should appear in at most one subtask\n` + - ` - Respect the ${MAX_DECOMP_TASKS}-subtask maximum`, - }], - }, - } - - // Extract decomposition JSON from thinker's response - const decompositionRaw: string = yield { - toolName: 'think_deeply', - input: { - prompt: - 'Look at the thinker agent\'s most recent response in this session. ' + - 'Extract ONLY the JSON array of subtasks it produced. ' + - 'Return just the raw JSON array, nothing else. ' + - 'If no valid JSON array is found, return an empty string.', - }, - } as unknown as string - - const subtasks = parseDecomposition(decompositionRaw, prompt) - - // ── Phase 2: Cascade wave execution ─────────────────────────────────────── - const agentConfigs = subtasks.map(st => { - let customPrefix = '' - if (st.specialist === 'custom') { - customPrefix = - `DYNAMIC SPECIALIST ROLE: ${st.customRole ?? 'Custom Specialist'}\n` + - `${st.customSystemAddition ?? ''}\n\n` - } - - return { - agent_type: resolveAgent(st.specialist), - prompt: - customPrefix + - COT_SYSTEM_PREFIX + - `SUBTASK [${st.focus}]:\n${st.subtask}\n\n` + - `FULL TASK CONTEXT (for reference only — implement only your subtask):\n${prompt}`, - } - }) - - const waves = splitIntoWaves(agentConfigs, MAX_WAVE_SIZE) - - for (let waveIdx = 0; waveIdx < waves.length; waveIdx++) { - const wave = waves[waveIdx] - - yield { - toolName: 'spawn_agents', - input: { agents: wave }, - } - - // Inter-wave integration review (not after the final wave) - if (waveIdx < waves.length - 1) { - yield { - toolName: 'spawn_agents', - input: { - agents: [{ - agent_type: 'codebuff/reviewer@0.0.1', - prompt: - `Inter-wave integration review (after Wave ${waveIdx + 1} of ${waves.length}).\n\n` + - `${wave.length} agents just completed work on: ${prompt}\n\n` + - `Before Wave ${waveIdx + 2} begins, check for:\n` + - ` 1. Conflicting changes that would break Wave ${waveIdx + 2} agents' work\n` + - ` 2. Exported symbols that Wave ${waveIdx + 2} agents will depend on\n` + - ` 3. Type mismatches or interface changes that need propagating\n` + - ` 4. Any incomplete implementations that would block the next wave\n` + - `Fix blockers now. Do not refactor style or non-blocking issues.`, - }], - }, - } - } - } - - // ── Phase 3: Final synthesis review ─────────────────────────────────────── - yield { - toolName: 'spawn_agents', - input: { - agents: [{ - agent_type: 'codebuff/reviewer@0.0.1', - prompt: - `Final synthesis review for a ${waves.length}-wave parallel cascade session.\n\n` + - `${subtasks.length} specialist agents completed work on:\n${prompt}\n\n` + - `Check specifically for:\n` + - ` 1. Conflicting changes between agents (same file edited inconsistently)\n` + - ` 2. Naming/interface inconsistencies across the codebase\n` + - ` 3. Missing integration glue between subsystems\n` + - ` 4. Any subtask that appears incomplete\n` + - ` 5. TODOs or placeholder comments left by any agent\n` + - `Fix all issues found — do not just report them.`, - }], - }, - } - - // ── Phase 4: Regex guard ─────────────────────────────────────────────────── - yield { - toolName: 'spawn_agents', - input: { - agents: [{ - agent_type: 'metabuff-regex-guard', - prompt: `Run regex guard for all code generated in the mega task: ${prompt}`, - }], - }, - } - - // ── Phase 5: Full typecheck + tests ──────────────────────────────────────── - yield { - toolName: 'spawn_agents', - input: { - agents: [{ - agent_type: 'basher', - params: { - command: - 'echo "=== TYPE CHECK ===" && ' + - '(bun run typecheck 2>/dev/null || npx tsc --noEmit 2>&1) | head -40 && ' + - 'echo "=== TESTS ===" && ' + - '(bun test 2>&1 || npx vitest run 2>&1 || npx jest 2>&1) | tail -30', - what_to_summarize: - 'Type-check and test results. ' + - 'Report any TypeScript errors or test failures. ' + - 'If errors found, describe them so the validator can fix them.', - timeout_seconds: BASHER_TIMEOUT, - }, - }], - }, - } - - // ── Phase 6: Final validation ────────────────────────────────────────────── - yield { - toolName: 'spawn_agents', - input: { - agents: [{ - agent_type: 'metabuff-validator', - prompt: - `Final validation pass for mega cascade task (${waves.length} waves, ` + - `${subtasks.length} specialists): ${prompt}`, - }], - }, - } - }, -} - -export default definition diff --git a/.agents/metabuff-reasoner.ts b/.agents/metabuff-reasoner.ts deleted file mode 100644 index cf33f69..0000000 --- a/.agents/metabuff-reasoner.ts +++ /dev/null @@ -1,171 +0,0 @@ -/** - * MetaBuff Reasoner — Deep Logic Specialist v1.0.0 - * ─────────────────────────────────────────────────── - * Handles tasks that require genuine algorithmic thinking rather than - * pattern matching. Uses maximum reasoning effort + 6-step Socratic - * protocol to close the single-model reasoning gap vs Claude Opus 4.8. - * - * TARGET TASKS (base agent: ~55/100, this agent targets: ~75+): - * • Novel algorithm design (sorting, graph, DP, string parsing) - * • Complex state machine logic (auth flows, multi-step workflows) - * • Performance optimization (bottleneck analysis, algorithmic improvements) - * • Mathematical / numerical computation (precision, overflow, rounding) - * • Concurrency and race condition analysis - * • API design with non-trivial constraint satisfaction - * • Parsers, transpilers, and transformation pipelines - * • Dynamic programming / memoization problems - * - * PROTOCOL: 6-step Socratic method - * 1. UNDERSTAND — restate the problem; identify inputs/outputs/constraints - * 2. CHALLENGE — critique the naive solution before accepting it - * 3. EXPLORE — generate 2–3 alternative approaches - * 4. SELECT — choose best and justify with explicit trade-off analysis - * 5. IMPLEMENT — execute with surgical edits + inline complexity annotations - * 6. PROVE — write the test that would catch the bug you almost made - * - * Spawned by: - * • metabuff.ts — when isAlgorithmTask is true in the complex pipeline - * • metabuff-mega.ts — for subtasks tagged specialist: 'reason' - * - * INLINING NOTE: - * No handleSteps — reasoning behaviour is driven entirely by - * systemPrompt + instructionsPrompt, which the agent reads on every step. - */ - -import { AgentDefinition } from './types/agent-definition' - -const FREE_MODEL = 'deepseek/deepseek-v4-pro' // Primary; falls back to deepseek-v4-flash when unavailable - -const REASONER_SYSTEM = `You are MetaBuff's deep reasoning specialist. -You are invoked for tasks that require genuine algorithmic thinking — not code lookup or boilerplate. - -CORE PRINCIPLE: - Never accept the first solution that comes to mind. - The "obvious" solution is where bugs hide. Your job is to find them first. - -REASONING STANDARDS (apply to every task): - • Time complexity — state Big-O and justify with a brief argument - • Space complexity — state Big-O and identify dominant allocation - • Edge cases — enumerate ALL before writing a single line of code - • Correctness — for non-trivial algorithms, argue why the approach is correct - • Test-first — write the test you'd use to verify correctness before the impl - -GROUNDING RULES (same as all MetaBuff agents — stricter enforcement here): - ✗ Do not hallucinate standard-library methods — verify via code_searcher or basher - ✗ Do not assume language semantics — test edge cases in basher node/python/go - ✗ Do not leave proofs as TODO — incomplete reasoning is a shipped bug - ✗ Do not choose O(n²) when O(n log n) is achievable without complexity trade-offs - ✗ Do not write a loop that could be off-by-one without proving the bounds - -CALIBRATION: - State every uncertainty explicitly before proceeding: - "⚠ UNCERTAIN [topic]: I'm choosing [A] over [B] because [reason]. - I'll validate this assumption via [tool/test] before continuing."` - -const REASONER_INSTRUCTIONS = ` -STEP 1 — UNDERSTAND (mandatory, do not skip) - • Restate the problem in your OWN words (not copied from the prompt) - • Identify: inputs, expected outputs, hard constraints, performance targets - • List every assumption you are making — each one is a potential bug - • Ask explicitly: "What would a WRONG implementation look like?" (to avoid it) - -STEP 2 — CHALLENGE (devil's advocate, do not skip) - • State the naive / obvious solution in one sentence - • Identify at least 2 failure modes of the naive solution: - - Correctness failures (wrong output for edge cases) - - Performance failures (O(n²) where O(n log n) is needed) - - Safety failures (integer overflow, floating-point precision loss, race condition) - • State the baseline complexity of the naive solution - -STEP 3 — EXPLORE - • Generate 2–3 fundamentally different approaches - • For each, state: - - Core idea in one sentence - - Time complexity (and proof sketch) - - Space complexity - - Strengths and weaknesses - • Consider opposites: iterative ↔ recursive, greedy ↔ DP, exact ↔ approximate - • Consider data structure choices: array ↔ map ↔ tree ↔ graph - -STEP 4 — SELECT - • Choose the best approach given the stated constraints - • Write: "I chose [X] over [Y] and [Z] because: [specific reason]" - • State explicitly what you are trading off (speed? memory? code clarity?) - • If the choice is close, state: "⚠ UNCERTAIN: both [X] and [Y] are viable. - I'll proceed with [X] and add a comment explaining why." - -STEP 5 — IMPLEMENT - 1. Read all relevant existing code FIRST: - • read_files for every file you plan to touch - • code_searcher for every function, type, or symbol you plan to call - 2. Write the TEST CASE before the implementation: - • The test should fail on a naive/wrong implementation - • The test should pass only if the algorithm is correct - 3. Implement with inline annotations: - • // O(n) — linear scan; each element visited once - • // Invariant: lo ≤ mid ≤ hi at every iteration - • // Edge case: empty input returns early here - 4. After each function: state its preconditions and postconditions as comments - -STEP 6 — PROVE - 1. Run the tests via basher: - bun test [test-file] OR npx vitest run [test-file] OR npx jest [test-file] - 2. Manually trace the algorithm with your HARDEST edge case: - (empty input / single element / max-size / all-same / already sorted / reversed) - 3. State explicitly: "This is correct because [argument]" - 4. Run typecheck: (bun run typecheck 2>/dev/null || npx tsc --noEmit 2>&1) | head -20 - 5. If any step fails → fix BEFORE calling end_turn - -GROUNDING REMINDER (before every tool call): - code_searcher for every function, type, or library method you plan to use. - Never write an import you haven't confirmed exists in this project.` - -const definition: AgentDefinition = { - id: 'metabuff-reasoner', - version: '1.0.0', - displayName: 'MetaBuff Deep Reasoner', - - spawnerPrompt: - 'Spawn for tasks requiring genuine algorithmic thinking: novel algorithm design, ' + - 'complex state machines, performance optimization, mathematical computation, ' + - 'concurrency / race condition analysis, parsers, or any problem where the naive ' + - 'solution is known to be wrong. Uses maximum reasoning effort and a 6-step ' + - 'Socratic protocol (understand → challenge → explore → select → implement → prove).', - - model: FREE_MODEL, - - reasoningOptions: { - enabled: true, - exclude: false, - effort: 'high', // Always max — this is the point of the reasoner - }, - - toolNames: [ - 'read_files', - 'code_searcher', - 'file_picker', - 'str_replace', - 'write_file', - 'basher', - 'glob', - 'think_deeply', - 'spawn_agents', - 'end_turn', - ], - - spawnableAgents: [ - 'codebuff/thinker@0.0.1', // escalation for especially thorny problems - ], - - includeMessageHistory: true, - - systemPrompt: REASONER_SYSTEM, - instructionsPrompt: REASONER_INSTRUCTIONS, - - stepPrompt: - 'Continue reasoning through the Socratic protocol. ' + - 'If you are on STEP 5 or 6, run your tests before calling end_turn. ' + - 'Do not call end_turn while any ⚠ UNCERTAIN items are unresolved or any tests are failing.', -} - -export default definition diff --git a/.agents/metabuff-regex-guard.ts b/.agents/metabuff-regex-guard.ts deleted file mode 100644 index 2f34636..0000000 --- a/.agents/metabuff-regex-guard.ts +++ /dev/null @@ -1,230 +0,0 @@ -/** - * MetaBuff Regex Guard — Pattern Safety Validator v1.0.0 - * ─────────────────────────────────────────────────────── - * Catches an entire class of bugs that TypeScript's type checker CANNOT: - * runtime-invalid regex patterns embedded in syntactically valid TS/JS code. - * - * Example: `new RegExp('\\d+{,3}')` compiles fine in TypeScript but throws - * `SyntaxError: Invalid regular expression` at runtime. - * - * WHAT IT CATCHES: - * 1. Invalid regex syntax — patterns that throw SyntaxError at runtime - * 2. Unclosed groups/classes — missing ) ] } in regex literals - * 3. Invalid flags — flag characters outside [gimsuy] in TS/JS - * 4. ReDoS patterns — nested quantifiers like (a+)+ causing catastrophic backtracking - * 5. Bad string-based regexes — incorrect double-escaping in new RegExp('...') - * 6. Empty alternation — (a||b) unintentionally matches empty string - * 7. Unanchored validators — forgot ^ or $ in patterns meant to validate full strings - * - * WHERE SPAWNED: - * • metabuff.ts — after basher typecheck in both simple and complex pipelines - * • metabuff-mega.ts — after synthesis review in the mega pipeline - * • metabuff-validator.ts — as an additional spawn when regex-adjacent patterns found - * - * SAFETY GUARANTEE: - * AI agents (including DeepSeek V4 Flash) frequently generate regex patterns that are - * syntactically plausible but semantically broken. This guard runs independently of - * the TypeScript compiler and catches issues the compiler misses. - * - * INLINING NOTE: - * REGEX_SCAN_COMMAND and REDOS_CHECK_PATTERNS are module-level constants - * used in SYSTEM/INSTRUCTIONS props (evaluated at import time) — safe. - * No handleSteps — behaviour driven entirely by systemPrompt + instructionsPrompt. - */ - -import { AgentDefinition } from './types/agent-definition' - -const FREE_MODEL = 'deepseek/deepseek-v4-pro' // Primary; falls back to deepseek-v4-flash when unavailable - -/** - * Shell script that validates regex patterns in all changed .ts/.tsx/.js files. - * - * Strategy: - * 1. Collect changed (tracked) + new (untracked) .ts/.tsx/.js files - * 2. Extract regex literals with grep (handles /pattern/flags syntax) - * 3. Validate each via Node.js new RegExp() — the same engine JS uses at runtime - * 4. Check for ReDoS-prone nested quantifiers - * 5. Check for new RegExp(string) calls with suspicious escape sequences - * - * NOTE: The grep pattern captures most literal regexes but not dynamic regexes - * built via string concatenation. Those require code_searcher inspection. - */ -const REGEX_SCAN_COMMAND = [ - // Collect files - 'TS_CHANGED=$(git diff HEAD --name-only 2>/dev/null | grep -E "\\.(ts|tsx|js|jsx)$")', - 'TS_NEW=$(git ls-files --others --exclude-standard 2>/dev/null | grep -E "\\.(ts|tsx|js|jsx)$")', - 'ALL_FILES=$(printf "%s\\n%s" "$TS_CHANGED" "$TS_NEW" | grep -v "^$" | sort -u)', - 'if [ -z "$ALL_FILES" ]; then echo "REGEX GUARD: No JS/TS files changed."; exit 0; fi', - 'echo "=== REGEX GUARD v1.0.0: Scanning changed files ==="', - 'GUARD_ERRORS=0', - - // Phase 1: Validate regex literal syntax via Node - 'echo "--- Phase 1: Regex literal syntax ---"', - 'for FILE in $ALL_FILES; do', - ' [ -f "$FILE" ] || continue', - ' PATTERNS=$(grep -oP "(?])\/(?:[^\\/\\n\\r]|\\\\.)+\/[gimsuy]*(?=[^a-zA-Z]|$)" "$FILE" 2>/dev/null | head -50 || true)', - ' [ -z "$PATTERNS" ] && continue', - ' while IFS= read -r PAT; do', - ' [ -z "$PAT" ] && continue', - ' INNER=$(echo "$PAT" | sed -E "s|^/(.+)/[gimsuy]*$|\\1|")', - ' RESULT=$(node -e "try{new RegExp(String.raw\\`$INNER\\`);process.exit(0)}catch(e){console.log(\\"❌ INVALID in $FILE: $PAT → \\"+e.message);process.exit(1)}" 2>&1)', - ' if [ $? -ne 0 ]; then echo "$RESULT"; GUARD_ERRORS=$((GUARD_ERRORS+1)); fi', - ' done <<< "$PATTERNS"', - 'done', - - // Phase 2: ReDoS detection — nested quantifiers - 'echo "--- Phase 2: ReDoS pattern check ---"', - 'for FILE in $ALL_FILES; do', - ' [ -f "$FILE" ] || continue', - ' REDOS=$(grep -nP "\\([^)]+[+*]\\)[+*{]|\\([^)]+\\)\\{[0-9]+," "$FILE" 2>/dev/null || true)', - ' if [ -n "$REDOS" ]; then', - ' echo "⚠️ POTENTIAL ReDoS in $FILE (nested quantifiers):"', - ' echo "$REDOS" | head -5', - ' GUARD_ERRORS=$((GUARD_ERRORS+1))', - ' fi', - 'done', - - // Phase 3: new RegExp(string) double-escape inspection - 'echo "--- Phase 3: new RegExp(string) escape check ---"', - 'for FILE in $ALL_FILES; do', - ' [ -f "$FILE" ] || continue', - ' BAD_ESC=$(grep -nP "new RegExp\\([\'\"]\\\\" "$FILE" 2>/dev/null | grep -vP "new RegExp\\([\'\"](\\\\\\\\[dwsWDS\\^$.|?*+()\\[\\]{}ntrbBfvuU0])" || true)', - ' if [ -n "$BAD_ESC" ]; then', - ' echo "⚠️ Suspicious single-backslash in new RegExp() in $FILE:"', - ' echo "$BAD_ESC" | head -5', - ' echo " Hint: In new RegExp strings, \\\\d must be written as \\\\\\\\d"', - ' fi', - 'done', - - // Phase 4: Empty alternation check - 'echo "--- Phase 4: Empty alternation check ---"', - 'for FILE in $ALL_FILES; do', - ' [ -f "$FILE" ] || continue', - ' EMPTY_ALT=$(grep -nP "/[^/]*\\|\\|[^/]*/" "$FILE" 2>/dev/null || true)', - ' if [ -n "$EMPTY_ALT" ]; then', - ' echo "⚠️ Empty alternation (||) in regex in $FILE — matches empty string:"', - ' echo "$EMPTY_ALT" | head -3', - ' fi', - 'done', - - // Summary - 'echo "=== REGEX GUARD: Complete ==="', - 'if [ "$GUARD_ERRORS" -gt 0 ]; then', - ' echo "❌ REGEX GUARD FAILED — $GUARD_ERRORS error(s) need fixing"', - ' exit 1', - 'else', - ' echo "✅ REGEX GUARD PASSED — no syntax errors detected"', - 'fi', -].join('\n') - -const REGEX_GUARD_SYSTEM = `You are MetaBuff's regex safety specialist. -You close the gap between TypeScript type safety and runtime regex correctness. - -TypeScript will NOT catch these. You will: - 1. SyntaxError patterns — new RegExp('\\d+{,3}') compiles; throws at runtime - 2. ReDoS patterns — (email+)+ is valid syntax; O(2^n) matching time - 3. Escape mismatches — \\d in a RegExp string needs \\\\d; one backslash is wrong - 4. Empty alternation — /foo||bar/ accidentally matches empty string - 5. Missing anchors — /\\d+/ used as a full-string validator matches partial strings - -SCOPE: - • TypeScript regex literals: /pattern/flags - • RegExp constructor calls: new RegExp('pattern', 'flags') - • String.prototype methods with regex args: .match() .replace() .search() .split() .replaceAll() - • Dynamic regexes: new RegExp(variable + suffix) ← flag for human review - -FIX PROTOCOL: - 1. Run the REGEX_SCAN_COMMAND via basher to get all issues - 2. For each issue: read the file, understand the intent, fix the pattern - 3. Use str_replace — surgical, targeted changes only - 4. Add a comment: // REGEX: [what this pattern does and why the fix is correct] - 5. Re-run the scan on the fixed file to confirm the error is gone - -NEVER: - ✗ Change the INTENT of a regex — only fix syntax/safety issues - ✗ Silently skip a ReDoS warning without either fixing or documenting it - ✗ Leave a partially-fixed regex — verify it still matches what it's supposed to` - -const REGEX_GUARD_INSTRUCTIONS = ` -For your regex guard pass: - -1. Run the full scan via basher: - (The full shell script is included in your BASHER_SCAN section below) - -2. Triage each finding: - A. INVALID REGEX (❌) — must fix before end_turn - • Read the file to understand the intended pattern - • Fix the syntax error (unclosed group, bad escape, invalid quantifier) - • Re-run scan after fix to confirm ✅ - - B. ReDoS (⚠️) — must fix or document - • Determine if the pattern is genuinely catastrophic or a false positive - • If catastrophic: rewrite using non-backtracking equivalent or possessive quantifier - • If false positive: add // REGEX: ReDoS-safe because [explanation] - - C. Suspicious escape (⚠️) — investigate and fix if wrong - • Check if the pattern produces the intended matches via node -e - • Fix double-escape issues: '\\d' → '\\\\d' in new RegExp() strings - - D. Empty alternation (⚠️) — investigate and fix if unintentional - • Check if the empty match case is intentional - • If unintentional: remove one pipe character - -3. For each fix: narrate the change: - "✓ FIXED: [file]:[line] — [what was wrong] → [what the correct form is]" - -4. Final summary: - REGEX GUARD PASSED — [N patterns scanned, N issues fixed, 0 remaining] - OR - REGEX GUARD NEEDS HUMAN REVIEW — [list patterns too complex to auto-fix] - -──── BASHER_SCAN ──────────────────────────────────────────────── -Run this exact command via basher: - -${REGEX_SCAN_COMMAND} -────────────────────────────────────────────────────────────────` - -const definition: AgentDefinition = { - id: 'metabuff-regex-guard', - version: '1.0.0', - displayName: 'MetaBuff Regex Guard', - - spawnerPrompt: - 'Spawn after any code change involving regex patterns, string validation, ' + - 'URL matching, input parsing, or any feature where incorrect regex causes a ' + - 'runtime exception or security gap. ' + - 'ALSO spawn proactively on ALL AI-generated code — LLMs frequently produce ' + - 'runtime-invalid regex that TypeScript\'s type checker silently accepts.', - - model: FREE_MODEL, - - reasoningOptions: { - enabled: true, - exclude: false, - effort: 'medium', // Regex analysis is mechanical — medium effort sufficient - }, - - toolNames: [ - 'read_files', - 'code_searcher', - 'str_replace', - 'write_file', - 'basher', - 'glob', - 'end_turn', - ], - - spawnableAgents: [], // Standalone — no sub-agents needed - - includeMessageHistory: true, - - systemPrompt: REGEX_GUARD_SYSTEM, - instructionsPrompt: REGEX_GUARD_INSTRUCTIONS, - - stepPrompt: - 'Continue the regex audit. Fix all ❌ errors and ⚠️ ReDoS warnings. ' + - 'Call end_turn only when the basher scan reports REGEX GUARD PASSED ' + - 'with 0 errors remaining.', -} - -export default definition diff --git a/.agents/metabuff-security.ts b/.agents/metabuff-security.ts deleted file mode 100644 index b19b595..0000000 --- a/.agents/metabuff-security.ts +++ /dev/null @@ -1,137 +0,0 @@ -/** - * MetaBuff Security — Security Analyst Specialist - * ───────────────────────────────────────────────── - * Handles security-critical aspects of complex tasks: - * • Authentication and authorization flows - * • Input validation and sanitization - * • Secrets management (never hardcoded, always env vars) - * • SQL injection, XSS, and injection attack surfaces - * • Rate limiting and abuse prevention - * • Access control (who can see/do what) - * - * Spawned by metabuff-mega for the 'security' subtask category. - * Can also be spawned directly for security audits. - */ - -import { AgentDefinition } from './types/agent-definition' - -const FREE_MODEL = 'deepseek/deepseek-v4-pro' // Primary; falls back to deepseek-v4-flash when unavailable - -/** Common insecure patterns to search for and eliminate */ -const SECURITY_RED_FLAGS = [ - // Hardcoded secrets - 'password.*=.*["\'][^"\']+["\']', - 'secret.*=.*["\'][^"\']+["\']', - 'api_key.*=.*["\'][^"\']+["\']', - 'token.*=.*["\'][^"\']+["\']', - - // Dangerous operations - 'eval\\(', - 'innerHTML.*=', - 'dangerouslySetInnerHTML', - 'exec\\(', - - // SQL injection surfaces - '\\$\\{.*\\}.*WHERE', - 'query.*\\+.*req\\.', - - // Weak crypto - 'md5', 'sha1', 'Math.random.*token', 'Math.random.*secret', -] - -const definition: AgentDefinition = { - id: 'metabuff-security', - version: '1.0.0', - displayName: 'MetaBuff Security Analyst', - - spawnerPrompt: - 'Spawn for security-critical work: authentication, authorization, input validation, ' + - 'secrets management, SQL injection prevention, or any feature touching user data.', - - model: FREE_MODEL, - - reasoningOptions: { - enabled: true, - exclude: false, - effort: 'high', // security mistakes are expensive — always think hard - }, - - toolNames: [ - 'read_files', - 'code_searcher', - 'file_picker', - 'str_replace', - 'write_file', - 'basher', - 'spawn_agents', - 'think_deeply', - 'glob', - 'end_turn', - ], - - spawnableAgents: [ - 'codebuff/thinker@0.0.1', - ], - - systemPrompt: `You are MetaBuff's security specialist. -You treat every user input as potentially malicious until proven otherwise. - -YOUR FOCUS: - • Authentication: session management, JWT validation, OAuth flows - • Authorization: every route/action must check permissions before executing - • Input validation: reject bad data at the boundary, never in the core - • Secrets: environment variables only — never in source code or git - • SQL: parameterized queries only — never string concatenation - • XSS: sanitize before rendering any user-supplied HTML - • CSRF: verify tokens on all state-changing requests - • Rate limiting: protect all public endpoints from abuse - -SECURITY DEFAULTS (apply unless codebase explicitly opts out with a comment): - • Deny by default — if unsure, block - • Fail secure — errors should not grant access - • Least privilege — request only the permissions the code actually needs - • Defense in depth — don't rely on a single layer - -NEVER: - • Hardcode credentials, API keys, passwords, or secrets - • Use deprecated crypto (MD5, SHA-1, DES, RC4) - • Trust client-supplied IDs for authorization decisions - • Log sensitive data (passwords, tokens, PII)`, - - instructionsPrompt: ` -For your assigned security subtask: - -1. Audit the codebase for known vulnerabilities: -${SECURITY_RED_FLAGS.map((_, i) => ` code_searcher searchQueries: [{ pattern: "${SECURITY_RED_FLAGS[i].replace(/"/g, '\\"')}" }]`).join('\n')} - (and run code_searcher for the remaining patterns relevant to your subtask) - -2. Read all files related to authentication and authorization: - - Auth middleware, guards, decorators - - Route definitions (look for missing auth middleware) - - User model and session handling - -3. Check secrets management: - - Search for hardcoded credentials: use code_searcher with pattern "password.*=.*\\"" - - Verify .env.example exists and .env is in .gitignore - - Confirm all secret reads go through process.env or a secrets manager - -4. Implement your fixes: - - Add missing input validation at API boundaries - - Add missing authorization checks before data access - - Replace any hardcoded secrets with environment variable references - - Replace any raw SQL concatenation with parameterized queries - -5. Add security comments: - // SECURITY: [why this validation/check is necessary] - -6. Verify nothing is broken: - - Run the test suite after your changes - - Specifically run any auth-related tests`, - - stepPrompt: - 'Continue the security work. ' + - 'Fix all vulnerabilities you have identified. ' + - 'Call end_turn only when all red flags are resolved and tests pass.', -} - -export default definition diff --git a/.agents/metabuff-testgen.ts b/.agents/metabuff-testgen.ts deleted file mode 100644 index 1c9addf..0000000 --- a/.agents/metabuff-testgen.ts +++ /dev/null @@ -1,121 +0,0 @@ -/** - * MetaBuff TestGen — Test Generation Specialist - * ─────────────────────────────────────────────── - * Generates comprehensive test coverage for code changed during a MetaBuff - * pipeline run. Understands the project's existing test patterns and writes - * tests in the same style rather than inventing a new approach. - * - * • Unit tests for changed functions and classes - * • Integration tests for changed API endpoints - * • Edge case coverage (null, empty, boundary, error paths) - * • Mock/stub generation for external dependencies - * • Snapshot tests for UI components (React, Vue) - * - * Spawned by metabuff-mega for the 'testgen' subtask category. - * Can also be spawned directly for test coverage work. - */ - -import { AgentDefinition } from './types/agent-definition' - -const FREE_MODEL = 'deepseek/deepseek-v4-pro' // Primary; falls back to deepseek-v4-flash when unavailable - -const definition: AgentDefinition = { - id: 'metabuff-testgen', - version: '1.0.0', - displayName: 'MetaBuff Test Generator', - - spawnerPrompt: - 'Spawn to generate tests for changed or new code. ' + - 'Writes unit tests, integration tests, and edge-case coverage ' + - 'matching the project\'s existing test style.', - - model: FREE_MODEL, - - reasoningOptions: { - enabled: true, - exclude: false, - effort: 'medium', - }, - - toolNames: [ - 'read_files', - 'code_searcher', - 'file_picker', - 'write_file', - 'str_replace', - 'basher', - 'glob', - 'end_turn', - ], - - spawnableAgents: [], - - systemPrompt: `You are MetaBuff's test generation specialist. -You write tests that actually catch bugs — not tests that just exercise the happy path. - -TESTING PHILOSOPHY: - • Test behavior, not implementation — tests should survive refactors - • Cover the contract (public API), not the internals - • Every non-trivial function needs: happy path, error path, edge cases - • Mocks should be minimal — over-mocking makes tests useless - • Test names should read like specifications: "should return 404 when user not found" - -COVERAGE TARGETS (aim for): - • All public functions/methods: at least happy + error path - • All API endpoints: valid input, invalid input, unauthorized, not found - • All data transformations: identity, empty, boundary values - • All state machines: all transitions + invalid transitions - -MATCHING EXISTING STYLE: - Before writing any test, read 2-3 existing test files to understand: - - The test framework (Jest, Vitest, pytest, Go testing, etc.) - - The assertion style (expect().toBe vs assert.equal vs t.Equal) - - How mocks/stubs are set up (jest.mock, vi.mock, sinon, etc.) - - How async is handled (async/await, done callbacks, etc.) - - File naming convention (*.test.ts, *.spec.ts, _test.go, test_*.py) - -NEVER: - • Write tests that test implementation details (private methods, internal state) - • Write tests that always pass regardless of the code - • Copy the function's logic into the test - • Leave empty test bodies or pending tests in the final output`, - - instructionsPrompt: ` -For your test generation subtask: - -1. Discover the test setup: - - Use file_picker or glob to locate existing test files (e.g., glob("**/*.test.ts") for TypeScript, glob("**/test_*.py") for Python) - - Read 2-3 representative test files to learn the testing style - - Check package.json for the test runner and any test utilities - - Look for test helpers, factories, or fixtures - -2. Find the code that needs tests: - - Read all changed/new source files - - List every public function, class, and API endpoint they contain - -3. For each item that needs tests: - A. Check if a test file already exists — if so, ADD to it - B. If not, create a new test file following the naming convention - C. Write tests in this order: - - Happy path (typical correct usage) - - Error/edge cases (null, empty, invalid types, out-of-range) - - Boundary conditions (max length, zero, negative numbers) - - Concurrent access (if relevant) - -4. Run the tests immediately after writing them: - npx vitest run [your-test-file] 2>&1 | tail -30 - OR: npx jest [your-test-file] 2>&1 | tail -30 - OR: bun test [your-test-file] 2>&1 | tail -30 - -5. Fix any test failures before calling end_turn: - - If the test is wrong, fix the test - - If the SOURCE CODE is wrong, fix the source code and report it - - All tests must pass before you finish`, - - stepPrompt: - 'Continue generating tests. ' + - 'Run each test file as you write it. ' + - 'Call end_turn only when all tests are written and passing.', -} - -export default definition diff --git a/.agents/metabuff-validator.ts b/.agents/metabuff-validator.ts deleted file mode 100644 index 0e06ead..0000000 --- a/.agents/metabuff-validator.ts +++ /dev/null @@ -1,148 +0,0 @@ -/** - * MetaBuff Validator — Anti-Hallucination Layer v1.1.0 - * ────────────────────────────────────────────────────── - * Runs after every MetaBuff pipeline to catch and fix the most common - * DeepSeek Flash failure modes: - * - * 1. Ghost imports — references to non-existent modules/types - * 2. Phantom edits — str_replace that claims success but left file unchanged - * 3. Broken tests — changes that silently break existing tests - * 4. Incomplete TODOs — placeholder code left in production paths - * 5. Type drift — new code inconsistent with existing type contracts - * 6. [NEW v1.1.0] Regex errors — runtime-invalid patterns TypeScript misses - * 7. [NEW v1.1.0] Consistency assertions — exports match their declared shapes - * - * CHANGES FROM v1.0.0: - * • [SAFETY] Regex check added to AUDIT CHECKLIST — flags regex-containing - * changed files so metabuff-regex-guard can be spawned if needed. - * • [QUAL] Ghost import detection strengthened — now checks both named imports - * and default imports via code_searcher before marking clean. - * • [QUAL] Consistency assertion: checks that exported function signatures - * match any callers that were also modified in this session. - * • [QUAL] metabuff-regex-guard added to spawnableAgents — validator can - * delegate regex scanning rather than attempting manual pattern analysis. - */ - -import { AgentDefinition } from './types/agent-definition' - -const FREE_MODEL = 'deepseek/deepseek-v4-pro' // Primary; falls back to deepseek-v4-flash when unavailable - -const VALIDATOR_SYSTEM_PROMPT = `You are MetaBuff's anti-hallucination validator. -Your ONLY job is to audit changes made by other agents and fix any problems. - -You are skeptical. You assume errors exist until proven otherwise. - -AUDIT CHECKLIST (run every time — check each box explicitly): - □ Read every modified file in full — use read_files - □ Confirm each import statement is valid — use code_searcher to verify symbols exist - □ Confirm every function/type called exists — use code_searcher to look up definitions - □ Search for "TODO", "FIXME", "placeholder" — use code_searcher - □ Check for regex literals or new RegExp() calls — if found, spawn metabuff-regex-guard - □ Check caller/callee consistency — if a function signature changed, verify callers match - □ Run the test suite — spawn a basher agent - □ Run the TypeScript/language compiler — spawn a basher agent - □ Check for syntax errors by re-reading with fresh eyes - -FIX PROTOCOL: - • If you find a ghost import → correct it or remove it - • If you find a TODO/placeholder → implement it or raise an error - • If tests fail → diagnose the root cause and fix the source, not the test - • If a type is inconsistent → align it with the existing type contract - • If regex patterns exist in changed files → spawn metabuff-regex-guard before calling end_turn - • If a function signature changed → verify ALL callers were updated - • Never suppress an error — always surface and fix the root cause - -OUTPUT FORMAT: - After your audit, end with one of: - ✅ VALIDATION PASSED — list what you checked - ❌ VALIDATION FAILED — list what you found and what you fixed` - -const VALIDATOR_INSTRUCTIONS = ` -Audit all changes made in this session. Use these tools (all available in toolNames): - - basher → run terminal commands (git diff, typecheck, tests) - - code_searcher → search for patterns (TODO, FIXME, symbol lookup, regex literals) - - read_files → read file contents - - str_replace → edit files (prefer this) - - write_file → create new files - - spawn_agents → spawn codebuff/base for fix passes OR metabuff-regex-guard for regex - -STEPS: - -1. Use basher to get the git diff of changed files: - git diff HEAD - -2. Use read_files to load the current state of each changed file. - -3. Use code_searcher to run the self-consistency checklist from your system prompt: - a. For every import: verify the imported name exists in that module - b. For every function call: verify the function exists and its signature matches - c. Search for TODO/FIXME/placeholder strings - d. Search for /regex/ literals or new RegExp( in changed files - -4. REGEX CHECK (v1.1.0): - If any changed file contains regex literals (/pattern/flags) or new RegExp() calls, - spawn metabuff-regex-guard BEFORE calling end_turn: - spawn_agents([{ agent_type: 'metabuff-regex-guard', prompt: 'Scan changes for regex safety.' }]) - -5. CONSISTENCY CHECK (v1.1.0): - If any changed file exports a function/type, check whether callers in other changed - files still match the updated signature. Use code_searcher to find callers. - Fix any mismatches with str_replace. - -6. If issues are found, fix them using str_replace (prefer) or write_file. - -7. Re-run tests and compilation using basher after any fix: - • TypeScript: (bun run typecheck 2>/dev/null || npx tsc --noEmit 2>&1) | head -50 - • Jest/Vitest: (npx vitest run 2>&1 || npx jest 2>&1) | tail -30 - • Bun: bun test 2>&1 | tail -30 - • Go: go build ./... && go test ./... - • Python: python -m pytest --tb=short 2>&1 | tail -40 - -8. Report your findings in the format described in your system prompt.` - -const definition: AgentDefinition = { - id: 'metabuff-validator', - version: '1.1.0', - displayName: 'MetaBuff Anti-Hallucination Validator', - - spawnerPrompt: - 'Spawn after any MetaBuff coding pipeline to validate changes, ' + - 'catch ghost imports, phantom edits, broken tests, incomplete TODOs, ' + - 'runtime-invalid regex patterns, and function signature mismatches.', - - model: FREE_MODEL, - - reasoningOptions: { - enabled: true, - exclude: false, - effort: 'low', // Mechanical verification — low effort is fine - }, - - toolNames: [ - 'read_files', - 'code_searcher', - 'str_replace', - 'write_file', - 'spawn_agents', - 'suggest_followups', - 'basher', - ], - - spawnableAgents: [ - 'codebuff/base@0.0.1', // targeted fix passes - 'codebuff/thinker@0.0.1', // deep analysis of tricky failures - 'metabuff-regex-guard', // v1.1.0: regex safety scan - ], - - includeMessageHistory: true, - - systemPrompt: VALIDATOR_SYSTEM_PROMPT, - instructionsPrompt: VALIDATOR_INSTRUCTIONS, - - stepPrompt: - 'Continue auditing. ' + - 'If you have found and fixed all issues, output your final VALIDATION PASSED/FAILED summary and call end_turn. ' + - 'Do not call end_turn while there are unresolved issues or while a regex scan is pending.', -} - -export default definition diff --git a/.agents/metabuff.ts b/.agents/metabuff.ts deleted file mode 100644 index 42ef338..0000000 --- a/.agents/metabuff.ts +++ /dev/null @@ -1,681 +0,0 @@ -/** - * MetaBuff — Main Orchestrator v1.5.0 - * ───────────────────────────────────── - * Makes Freebuff (DeepSeek V4 Pro) behave closer to Claude Opus 4.8 / Antigravity 2.0 - * by enforcing chain-of-thought, routing tasks by complexity, and coordinating - * Codebuff's built-in agents + MetaBuff's own specialist subagents. - * - * CHANGES FROM v1.4.0: - * • [QUAL] Scope-aware complexity routing — analyzeComplexityWithScope() replaces - * analyzeComplexityWithScope(). A pre-flight basher greps the real codebase for - * keywords extracted from the prompt and counts matched files. Actual file count - * overrides keyword false positives: - * - 1 matched file → hard-caps score to simple (fixes 'refactor login button' FP) - * - 2–3 files + no complex keyword → simple (not complex) - * - 4–7 files → complex - * - 8–15 files → complex (high-end); 16+ → mega - * - Cross-cutting (files spread across 4+ dirs) adds escalation bonus - * - Reliability guard: if grep matches >40% of total project files, keywords - * are too generic → falls back to pure keyword scoring (safe for vague prompts) - * Adds 1 lightweight basher spawn (timeout: 15s) before every routing decision. - * - * CHANGES FROM v1.3.0: - * • [QUAL] isAlgorithmTask detector — routes novel-algorithm/complex-logic tasks - * to metabuff-reasoner (effort=high, Socratic protocol) instead of codebuff/planner. - * Closes single-model reasoning gap vs Claude Code Opus 4.8 (~55 → ~72/100). - * • [QUAL] withCoT upgraded to v2: added STEP 2.5 QUESTION (Socratic pre-flight). - * Agents must articulate 3 failure modes before writing code, cutting assumption- - * based hallucinations. - * • [SAFETY] metabuff-regex-guard added to BOTH simple and complex pipelines. - * TypeScript's type checker cannot catch runtime-invalid regex — this closes - * the entire class of regex SyntaxError bugs from AI-generated code. - * • [QUAL] Complex pipeline: reasoning effort escalated to 'high' for algorithm tasks - * (was always 'medium' via codebuff/planner). - * • [BUG] isRegexTask detector added — simple tasks only run regex-guard when the - * prompt involves patterns/parsing (avoids unnecessary spawn on pure refactors). - * • [PERF] Regex guard is a single basher-driven spawn — low overhead, no freeze risk. - * - * Architecture: - * Task → [Memory read + Complexity analysis] → MetaBuff orchestrator - * → Simple: base(CoT v2) → typecheck → [regex-guard] → validator [2–4 spawns] - * → Complex: file-picker → planner|reasoner(CoT v2) → reviewer [5–6 spawns] - * → typecheck → regex-guard → validator - * → Mega: metabuff-mega (cascade wave parallel spawning) [delegated] - * - * SAFETY FEATURES (v1.4.0): - * • All v1.3.0 safety features retained (saturation, diminishing returns, etc.) - * • Regex guard — catches runtime-invalid patterns TypeScript misses - * • Socratic pre-flight — agents articulate failure modes before coding - * • Algorithm task routing — harder reasoning uses higher-effort specialist - * • Timeout bounds on all basher commands — no infinite hangs - * • Targeted typecheck for simple tasks — not full codebase scan - * • Inter-session memory — known-issues.md as mandatory first step - * - * CRITICAL NOTE: - * All helpers (analyzeComplexity, withCoT, withReview, isAlgorithmTask, isRegexTask) - * are inlined inside handleSteps. The agent execution framework extracts only the - * exported definition object — module-level function references are NOT preserved. - */ - -import { AgentDefinition } from './types/agent-definition' - -const definition: AgentDefinition = { - id: 'metabuff', - version: '1.5.0', - displayName: 'MetaBuff Orchestrator', - - spawnerPrompt: - 'Spawn MetaBuff as your primary agent for ANY coding task. ' + - 'It automatically classifies complexity and coordinates the optimal agent pipeline, ' + - 'including CoT enforcement, inter-session memory, continuous validation, ' + - 'regex safety checks, and anti-hallucination protocols.', - - model: 'deepseek/deepseek-v4-pro', // Primary; falls back to deepseek-v4-flash when unavailable - - reasoningOptions: { - enabled: true, - exclude: false, - effort: 'medium', - }, - - toolNames: ['spawn_agents', 'think_deeply', 'end_turn'], - - spawnableAgents: [ - 'codebuff/base@0.0.1', - 'codebuff/file-picker@0.0.1', - 'codebuff/thinker@0.0.1', - 'codebuff/planner@0.0.1', - 'codebuff/reviewer@0.0.1', - 'codebuff/researcher@0.0.1', - 'basher', - 'metabuff-validator', - 'metabuff-reasoner', // v1.4.0: algorithm/logic specialist - 'metabuff-regex-guard', // v1.4.0: runtime regex safety - 'metabuff-mega', - ], - - systemPrompt: - 'You are MetaBuff, an intelligent orchestration layer that coordinates AI coding agents. ' + - 'Your job is NOT to write code yourself — it is to decompose tasks, select the right agents, ' + - 'and ensure every output is verified before delivery. ' + - 'Always prefer precision over speed.', - - handleSteps: function* ({ prompt }) { - - // ─── SAFETY BOUNDS ──────────────────────────────────────────────────────── - - /** Maximum complexity score — prevents runaway mega classification */ - const COMPLEXITY_SATURATION = 8 - - /** Timeout for basher commands in seconds */ - const BASHER_TIMEOUT = 60 - - // ─── HELPER: Complexity scorer (v1.5.0 — scope-aware) ──────────────────── - /** - * Combines keyword heuristics (v1.4 behaviour, kept as fallback) with - * real file-count data written by the pre-flight scope basher in Phase 0.5. - * - * scope.reliable is false when: - * • No meaningful keywords could be extracted from the prompt (vague task) - * • grep matched >40% of project files (keywords too generic, e.g. "file", "code") - * In those cases the function falls through to pure keyword scoring. - */ - function analyzeComplexityWithScope( - p: string, - scope: { matchedFiles: number; totalFiles: number; dirCount: number; reliable: boolean } - ): 'simple' | 'complex' | 'mega' { - const lower = p.toLowerCase() - let score = 0 - let megaHits = 0 - let complexHits = 0 - - // ── Tier 1: mega keywords ───────────────────────────────────────────── - const megaKw = [ - 'from scratch', 'entire codebase', 'full system', - 'complete rewrite', 'new architecture', 'all files', 'every file', - 'migrate entire', 'redesign everything', 'operating system', - ] - for (const kw of megaKw) { - if (lower.includes(kw)) { - megaHits++ - score += Math.max(1, 4 - (megaHits - 1) * 1.5) - } - } - - // ── Tier 2: complex keywords ────────────────────────────────────────── - const complexKw = [ - 'refactor', 'architecture', 'redesign', 'integrate', 'migrate', - 'add auth', 'add authentication', 'database migration', - 'all endpoints', 'all components', 'performance', - 'multiple files', 'across the', 'everywhere', - 'add new', 'create new', 'implement', - 'new api', 'new route', 'new endpoint', - 'new component', 'new page', 'new feature', - 'add tests', 'write tests', 'unit test', - ] - for (const kw of complexKw) { - if (lower.includes(kw)) { - complexHits++ - score += Math.max(0.5, 2 - (complexHits - 1) * 0.5) - } - } - - // ── Tier 3: explicit filenames in prompt text (v1.4 behaviour) ──────── - const fileMatches = p.match(/\b\w+\.(ts|tsx|js|jsx|py|go|rs|java|cpp|cs)\b/g) - if (fileMatches) { - const uniqueFiles = new Set(fileMatches) - if (uniqueFiles.size > 8) score += 2 - else if (uniqueFiles.size > 4) score += 1 - else if (uniqueFiles.size > 2) score += 0.5 - } - - // ── Tier 4: PascalCase component names (v1.4 behaviour) ────────────── - const componentMatches = p.match(/\b[A-Z][a-z]+[A-Z][a-zA-Z]*\b/g) - if (componentMatches) { - const uniqueComps = new Set(componentMatches) - if (uniqueComps.size > 3) score += 2 - else if (uniqueComps.size > 1) score += 1 - } - - // ── Tier 5: cross-cutting concern counter (v1.4 behaviour) ─────────── - const concerns = [ - 'api', 'database', 'db', 'schema', 'migration', 'config', - 'deploy', 'ci', 'component', 'auth', 'middleware', 'query', - ] - let concernCount = 0 - for (const c of concerns) { - if (lower.includes(c)) concernCount++ - } - if (concernCount > 4) score += 2 - else if (concernCount > 2) score += 1 - - if (p.length > 500) score += 1 - - // ── NEW v1.5.0: Real file-scope override ────────────────────────────── - // Applies only when the pre-flight grep was reliable (specific enough keywords, - // matched < 40% of the project). Actual codebase evidence beats keywords. - if (scope.reliable && scope.matchedFiles >= 0) { - const { matchedFiles, dirCount } = scope - - // Cross-cutting bonus: same file count is harder when spread across dirs - const crossCutting = dirCount >= 4 - - if (matchedFiles === 0) { - // Keywords had no grep hits — vague prompt, keep keyword score unchanged - } else if (matchedFiles === 1) { - // Definitively single-file — hard-cap to simple even if 'refactor' is present - // ("refactor the login button" touching 1 file IS a simple task) - score = Math.min(score, 1.9) - } else if (matchedFiles <= 3) { - // 2–3 files: keep at simple UNLESS keyword score already says complex - // ("refactor auth.ts and user.ts" with 2 files still warrants complex pipeline) - const fileScore = score >= 2 ? 2.5 : 1.5 - score = Math.max(score, fileScore) - } else if (matchedFiles <= 7) { - // 4–7 files: solidly complex - score = Math.max(score, 2.5) - } else if (matchedFiles <= 15) { - // 8–15 files OR 5+ files cross-cutting: high-end complex (approaching mega) - const fileScore = crossCutting ? 4.5 : 4 - score = Math.max(score, fileScore) - } else { - // 16+ files, or 10+ cross-cutting: mega territory - score = Math.max(score, 6) - } - } - - score = Math.min(score, COMPLEXITY_SATURATION) - - if (score >= 6) return 'mega' - if (score >= 2) return 'complex' - return 'simple' - } - - // ─── HELPER: Algorithm task detector ────────────────────────────────────── - /** - * Returns true when the task involves novel algorithmic reasoning - * that benefits from metabuff-reasoner (effort=high, Socratic 6-step). - * NEW v1.4.0 - */ - function isAlgorithmTask(p: string): boolean { - return /\b(algorithm|algorithms|parse|parser|sort(?:ing)?|search(?:ing)?|graph|tree|trie|heap|dp|dynamic.?programm|recursion|recursive|memoiz|optimiz(?:e|ation)|performance|time.?complex|space.?complex|big.?o|bigint|float(?:ing.?point)?|numeric|precision|overflow|concurrent|concurren|mutex|race.?condition|deadlock|state.?machine|workflow.?engine|transpil|compil(?:er|ation)|lexer|tokeniz|ast|backtrack|greedy)\b/i.test(p) - } - - // ─── HELPER: Regex/pattern task detector ────────────────────────────────── - /** - * Returns true when the task involves regex or string-pattern matching, - * triggering the regex guard even in the simple pipeline. - * NEW v1.4.0 - */ - function isRegexTask(p: string): boolean { - return /\b(regex|regexp|pattern|match(?:ing)?|replace(?:all)?|sanitiz|validat|parse|url.*match|email.*valid|phone.*valid|search.*pattern|string.*extract)\b/i.test(p) - } - - // ─── HELPER: Full CoT wrapper v2 ────────────────────────────────────────── - /** - * v1.4.0 upgrade: added STEP 2.5 QUESTION — Socratic pre-flight. - * Agents must articulate 3 failure modes before planning. - */ - function withCoT(task: string, role = 'coding'): string { - return ` -You are operating under MetaBuff's anti-hallucination protocol v2. - -BEFORE taking any action you MUST follow these steps IN ORDER: - -STEP 1 — ORIENT - • State the goal in one sentence - • List every file you need to read (don't assume contents you haven't seen) - -STEP 2 — GROUND - • Read all listed files via read_files - • Run code_searcher for any symbol, function, or type you plan to reference - • NEVER write an import path, class name, or API call you haven't verified - -STEP 2.5 — QUESTION ← Socratic pre-flight (do not skip) - • Ask yourself: "What are 3 specific ways this implementation could be wrong?" - • Ask yourself: "What am I assuming that I haven't yet verified with a tool call?" - • Does this task involve regex, string parsing, or pattern matching? - If yes → flag it with: "⚠ REGEX RISK — will verify all patterns after implementation" - • For each assumption: resolve it with a tool call BEFORE proceeding to STEP 3 - • Do NOT proceed to STEP 3 while any unresolved assumption exists - -STEP 3 — PLAN - • Write a numbered action plan (what changes in what files in what order) - • Flag any remaining uncertainty as: "⚠ UNCERTAIN: [thing you are not sure about]" - • Resolve all uncertainties with tool calls before proceeding - -STEP 4 — EXECUTE - • Carry out each step one at a time - • Use str_replace for targeted edits; write_file only for new files - • After each edit, narrate: "✓ DONE: [what changed and why it's correct]" - -STEP 5 — VERIFY - • Re-read changed files to confirm the edit landed correctly - • Run any available tests or lint commands via basher - • If anything looks wrong, fix it before calling end_turn - • If you flagged ⚠ REGEX RISK above: explicitly state that regex-guard will follow - -GROUNDING RULES (never violate): - ✗ Do not reference a file path without having read it this session - ✗ Do not assume a function or type exists — verify with code_searcher - ✗ Do not invent package names or import paths - ✗ Do not leave TODOs or placeholder code in the final output - ✗ Do not call end_turn if there are unresolved ⚠ UNCERTAIN items - - - -${task} -` - } - - // ─── HELPER: Light review wrapper ───────────────────────────────────────── - function withReview(task: string): string { - return ` -You are a reviewer in MetaBuff's pipeline. Your job is to VERIFY, not to re-implement. - -PROTOCOL: - 1. READ every changed file in this session (use read_files + git diff HEAD) - 2. CHECK for: syntax errors, missing imports, broken references, TODOs/placeholders - 3. FIX issues you find using str_replace (surgical, targeted changes only) - 4. REPORT what you checked and what (if anything) you fixed - -RULES: - ✗ Do not re-write large sections unless there is a concrete bug - ✗ Do not call end_turn while there are unfixed issues - ✗ Do not invent problems that aren't there - - - -${task} -` - } - - // ─── PHASE 0: INTER-SESSION MEMORY ──────────────────────────────────────── - yield { - toolName: 'spawn_agents', - input: { - agents: [{ - agent_type: 'basher', - params: { - command: - 'if [ -f .agents/known-issues.md ]; then ' + - ' echo "=== INTER-SESSION MEMORY ===" && ' + - ' cat .agents/known-issues.md; ' + - 'else ' + - ' echo "No known-issues.md found — first session."; ' + - 'fi', - what_to_summarize: - 'List all known issues from previous sessions. ' + - 'These will inform how all subsequent agents approach this task.', - timeout_seconds: 10, - }, - }], - }, - } - - // ─── PHASE 0.5: FILE SCOPE PRE-FLIGHT ──────────────────────────────────── - // Grep the real codebase for keywords extracted from the prompt. - // Writes { matchedFiles, totalFiles, dirCount, reliable } to .agents/.scope-tmp.json - // which analyzeComplexityWithScope() reads synchronously after this yield. - const safePromptForBash = prompt - .replace(/\\/g, '\\\\') - .replace(/'/g, "'\\''") - .replace(/\n/g, ' ') - .slice(0, 400) - - yield { - toolName: 'spawn_agents', - input: { - agents: [{ - agent_type: 'basher', - params: { - command: [ - // Extract meaningful keywords: >3 chars, lowercase, minus stop words - `KEYWORDS=$(echo '${safePromptForBash}' \\`, - ` | tr '[:upper:]' '[:lower:]' \\`, - ` | grep -oE '[a-zA-Z]{4,}' \\`, - ` | grep -vxE 'this|that|with|from|have|will|your|into|when|them|they|what|file|code|make|want|need|just|like|some|more|then|also|than|even|only|over|back|here|there|their|been|were|does|dont|should|would|could|change|update|every|other|same|such|very|much|many|well|still|down|first|last|next|always|often|across|without|within|along|through|around|between|during|against|inside|outside|toward|under|until|upon|while|since|each|both|about|above|below|where|after|before|already|again|never|using|please|really|just|simply|whether|something|anything|nothing|everything' \\`, - ` | sort -u | head -8 | tr '\\n' '|' | sed 's/|$//')`, - ``, - // Count total source files (excluding noise dirs) - `TOTAL=$(find . -type f \\( -name '*.ts' -o -name '*.tsx' -o -name '*.js' -o -name '*.jsx' -o -name '*.py' -o -name '*.go' -o -name '*.rs' -o -name '*.java' \\) \\`, - ` | grep -v node_modules | grep -v '\\.git' | grep -v dist | grep -v '\\.next' | grep -v coverage | grep -v __pycache__ \\`, - ` | wc -l | tr -d ' ')`, - ``, - // Grep codebase for matched files - `if [ -n "$KEYWORDS" ]; then`, - ` MATCHED=$(grep -rli --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.py' --include='*.go' --include='*.rs' \\`, - ` -E "$KEYWORDS" . 2>/dev/null \\`, - ` | grep -v node_modules | grep -v '\\.git' | grep -v dist | grep -v '\\.next' | wc -l | tr -d ' ')`, - ` # Count distinct directories of matched files (cross-cutting signal)`, - ` DIRS=$(grep -rli --include='*.ts' --include='*.tsx' --include='*.js' --include='*.py' \\`, - ` -E "$KEYWORDS" . 2>/dev/null \\`, - ` | grep -v node_modules | grep -v '\\.git' | grep -v dist \\`, - ` | sed 's|/[^/]*$||' | sort -u | wc -l | tr -d ' ')`, - ` # Reliability check: if matched > 40% of total, keywords are too generic`, - ` THRESH=$(( TOTAL * 2 / 5 + 1 ))`, - ` if [ "$TOTAL" -gt 0 ] && [ "$MATCHED" -le "$THRESH" ]; then RELIABLE=true; else RELIABLE=false; fi`, - `else`, - ` MATCHED=0; DIRS=0; RELIABLE=false`, - `fi`, - ``, - // Write JSON result - `printf '{"matchedFiles":%s,"totalFiles":%s,"dirCount":%s,"reliable":%s}\\n' \\`, - ` "$MATCHED" "$TOTAL" "$DIRS" "$RELIABLE" > .agents/.scope-tmp.json`, - `echo "scope: ${safePromptForBash}" `, - `echo "scope-result: matched=$MATCHED total=$TOTAL dirs=$DIRS reliable=$RELIABLE keys=$KEYWORDS"`, - ].join('\n'), - what_to_summarize: - 'File scope pre-flight: report how many files matched and whether reliable.', - timeout_seconds: 15, - }, - }], - }, - } - - // Read scope data synchronously before routing. - // handleSteps is a synchronous generator — code between yields runs in the - // main Node.js thread, so readFileSync is safe here. - let scopeData: { matchedFiles: number; totalFiles: number; dirCount: number; reliable: boolean } = { - matchedFiles: -1, totalFiles: 0, dirCount: 0, reliable: false, - } - try { - // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires - const fs = require('fs') - const raw: string = fs.readFileSync('.agents/.scope-tmp.json', 'utf8') - scopeData = JSON.parse(raw) - try { fs.unlinkSync('.agents/.scope-tmp.json') } catch { /* cleanup best-effort */ } - } catch { - // Scope file unavailable (first run, timeout, or unsupported env) - // analyzeComplexityWithScope falls back to keyword-only scoring when reliable=false - } - - // ─── PHASE 1: COMPLEXITY ANALYSIS ───────────────────────────────────────── - const complexity = analyzeComplexityWithScope(prompt, scopeData) - - const isGenerationTask = /\b(create new file|write new file|generate.*\.(ts|tsx|js|jsx|py)|new component|new page|from scratch|build.*system)\b/i.test(prompt) - const algoTask = isAlgorithmTask(prompt) - const regexTask = isRegexTask(prompt) - - // ── SIMPLE ──────────────────────────────────────────────────────────────── - if (complexity === 'simple') { - - yield { - toolName: 'spawn_agents', - input: { - agents: [{ - agent_type: 'codebuff/base@0.0.1', - prompt: withCoT(prompt), - }], - }, - } - - // Targeted typecheck — only changed + new files - yield { - toolName: 'spawn_agents', - input: { - agents: [{ - agent_type: 'basher', - params: { - command: - 'TS_CHANGED=$(git diff HEAD --name-only 2>/dev/null | grep -E "\\.(ts|tsx)$") && ' + - 'TS_NEW=$(git ls-files --others --exclude-standard 2>/dev/null | grep -E "\\.(ts|tsx)$") && ' + - 'if [ -n "${TS_CHANGED}${TS_NEW}" ]; then ' + - ' echo "=== TYPECHECK (changed + new files) ===" && ' + - ' (bun run typecheck 2>/dev/null || npx tsc --noEmit 2>&1) | head -30; ' + - 'else ' + - ' echo "No .ts/.tsx files changed or created — skipping typecheck."; ' + - 'fi', - what_to_summarize: - 'Report any TypeScript errors in changed files. ' + - 'If errors exist, fix them before end_turn.', - timeout_seconds: BASHER_TIMEOUT, - }, - }], - }, - } - - // v1.4.0: Regex guard — only for regex/pattern tasks or generation - if (regexTask || isGenerationTask) { - yield { - toolName: 'spawn_agents', - input: { - agents: [{ - agent_type: 'metabuff-regex-guard', - prompt: `Run regex guard for changes in: ${prompt}`, - }], - }, - } - } - - if (isGenerationTask) { - yield { - toolName: 'spawn_agents', - input: { - agents: [{ - agent_type: 'basher', - params: { - command: - 'echo "=== NEW FILES ===" && ' + - 'git diff HEAD --name-only --diff-filter=A 2>/dev/null | head -10 || ' + - 'echo "No new files detected"', - what_to_summarize: - 'List newly created files. ' + - 'Verify each new .ts/.tsx file has valid imports and no syntax errors.', - timeout_seconds: 15, - }, - }], - }, - } - } - - yield { - toolName: 'spawn_agents', - input: { - agents: [{ - agent_type: 'metabuff-validator', - prompt: `Validate changes made for: ${prompt}`, - }], - }, - } - - // ── COMPLEX ─────────────────────────────────────────────────────────────── - } else if (complexity === 'complex') { - - yield { - toolName: 'spawn_agents', - input: { - agents: [{ - agent_type: 'codebuff/file-picker@0.0.1', - prompt: `Find all files relevant to: ${prompt}`, - }], - }, - } - - // v1.4.0: Algorithm tasks → reasoner (effort=high, Socratic 6-step) - // Normal tasks → planner with CoT v2 - if (algoTask) { - yield { - toolName: 'spawn_agents', - input: { - agents: [{ - agent_type: 'metabuff-reasoner', - prompt: - `Task requires algorithmic reasoning — apply the full 6-step Socratic protocol.\n\n` + - `Task: ${prompt}\n\n` + - `CONTEXT: Focus on correctness proofs and complexity analysis. ` + - `Run tests at STEP 6 before calling end_turn.`, - }], - }, - } - } else { - yield { - toolName: 'spawn_agents', - input: { - agents: [{ - agent_type: 'codebuff/planner@0.0.1', - prompt: withCoT( - `Analyze the full scope, then implement all changes for:\n${prompt}\n\n` + - `Before writing a single line, identify ALL files that need to change ` + - `and produce a dependency-ordered change list. Flag every assumption.` - ), - }], - }, - } - } - - yield { - toolName: 'spawn_agents', - input: { - agents: [{ - agent_type: 'codebuff/reviewer@0.0.1', - prompt: withReview( - `Review ALL changes made for: ${prompt}\n\n` + - `Also check:\n` + - ` - Syntax errors and missing imports\n` + - ` - TODOs and placeholder code\n` + - ` - Broken references or non-existent symbols\n` + - `Fix anything you find. Do not just report it.` - ), - }], - }, - } - - yield { - toolName: 'spawn_agents', - input: { - agents: [{ - agent_type: 'basher', - params: { - command: - 'echo "=== TYPE CHECK ===" && ' + - '(bun run typecheck 2>/dev/null || npx tsc --noEmit 2>&1) | head -40 && ' + - 'echo "=== TESTS ===" && ' + - '(bun test 2>&1 || npx vitest run 2>&1 || npx jest 2>&1) | tail -30', - what_to_summarize: - 'Type-check and test results. ' + - 'Report any TypeScript errors or test failures. ' + - 'If errors found, fix them now before calling end_turn.', - timeout_seconds: BASHER_TIMEOUT, - }, - }], - }, - } - - // v1.4.0: Regex guard always runs in the complex pipeline - yield { - toolName: 'spawn_agents', - input: { - agents: [{ - agent_type: 'metabuff-regex-guard', - prompt: `Run regex guard for all changes in: ${prompt}`, - }], - }, - } - - if (isGenerationTask) { - yield { - toolName: 'spawn_agents', - input: { - agents: [{ - agent_type: 'basher', - params: { - command: - 'echo "=== SANDBOX ===" && ' + - 'git diff HEAD --name-only --diff-filter=A 2>/dev/null | head -20 || ' + - 'echo "No new files detected"', - what_to_summarize: - 'List newly created files. ' + - 'Verify any new source files have proper imports and no syntax errors.', - timeout_seconds: 15, - }, - }], - }, - } - } - - yield { - toolName: 'spawn_agents', - input: { - agents: [{ - agent_type: 'metabuff-validator', - prompt: `Validate all changes for: ${prompt}`, - }], - }, - } - - // ── MEGA ────────────────────────────────────────────────────────────────── - } else { - - yield { - toolName: 'spawn_agents', - input: { - agents: [{ - agent_type: 'metabuff-mega', - prompt: prompt, - }], - }, - } - - // Post-mega conflict resolution - yield { - toolName: 'spawn_agents', - input: { - agents: [{ - agent_type: 'codebuff/base@0.0.1', - prompt: withReview( - `Post-mega conflict check for: ${prompt}\n\n` + - `Multiple specialist agents ran in parallel. Check specifically for:\n` + - ` 1. Conflicting changes between agents (same file modified inconsistently)\n` + - ` 2. Missing integration glue between subsystems\n` + - ` 3. Any TODOs or placeholder comments left by agents\n` + - `Fix ALL issues found.` - ), - }], - }, - } - } - }, -} - -export default definition diff --git a/.agents/skills/context7/SKILL.md b/.agents/skills/context7/SKILL.md deleted file mode 100644 index e5c592b..0000000 --- a/.agents/skills/context7/SKILL.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -name: context7 -description: Upstash Context7 integration — fetches live, version-specific documentation for any library/framework and injects it directly into the agent's context. Prevents hallucinated APIs by replacing stale training data with current docs. -license: MIT -metadata: - author: Upstash - version: "1.0.0" ---- - -# Context7 — Live Documentation for AI Agents - -This skill integrates **Upstash Context7** into the Codebuff workflow. Context7 fetches up-to-date, version-specific documentation and code examples straight from the source, placing them directly into the agent's prompt context. - -## Why This Matters - -This project uses very recent framework versions: -- **Next.js 16** (App Router) -- **React 19** -- **Prisma 7** (with `@prisma/adapter-pg`) -- **Tailwind CSS v4** (with `@tailwindcss/postcss` plugin) - -AI training data may predate API changes in these versions. Context7 solves this by pulling live docs. - -## How to Use - -### Step 1: Resolve a library to its Context7 ID - -```bash -npx ctx7 library "" -``` - -Examples: -```bash -npx ctx7 library prisma "How to define one-to-many relations with cascade delete" -npx ctx7 library nextjs "How to set up app router with middleware" -npx ctx7 library react "How to clean up useEffect with async operations" -``` - -### Step 2: Fetch documentation - -Use the Library ID from Step 1 to fetch live documentation: - -```bash -npx ctx7 docs "" -``` - -Examples: -```bash -npx ctx7 docs /prisma/web "How to use @prisma/adapter-pg with connection pooling" -npx ctx7 docs /vercel/next.js "How to use React cache() for SSR deduplication" -npx ctx7 docs /facebook/react "How to use useActionState in React 19" -npx ctx7 docs /tailwindlabs/tailwindcss "How to set up v4 with @tailwindcss/postcss" -``` - -## Version-Specific Queries - -When you know the exact version, specify it for more precise results: - -```bash -npx ctx7 docs /vercel/next.js/v16.0.0 "How to configure Turbopack" -npx ctx7 docs /facebook/react/v19.0.0 "How to use Server Components" -``` - -## Output JSON (for scripting) - -Both `ctx7 library` and `ctx7 docs` support `--json` flag for structured output: - -```bash -npx ctx7 library prisma "relations" --json -npx ctx7 docs /prisma/web "cascade delete" --json -``` - -## When to Always Use Context7 - -**ALWAYS** use Context7 before writing code that involves: -- Framework APIs (Next.js route handlers, layouts, metadata, middleware) -- ORM queries (Prisma relations, raw queries, connection pooling, migrations) -- UI libraries (Tailwind CSS v4, Framer Motion, lucide-react) -- React hooks and patterns (`useActionState`, `useOptimistic`, Server Components, `cache()`) -- Database (PostgreSQL, Supabase, pg.Pool) -- Package configuration (`next.config.mjs`, `postcss.config.mjs`, `tsconfig.json`) - -## Verification - -After writing code using Context7 docs, cross-reference with the project's existing patterns to ensure consistency. The goal is **correctness + consistency** — use live docs for API accuracy, but match the existing project's style and conventions. diff --git a/.agents/types/agent-definition.ts b/.agents/types/agent-definition.ts deleted file mode 100644 index 7720240..0000000 --- a/.agents/types/agent-definition.ts +++ /dev/null @@ -1,20 +0,0 @@ -export interface AgentDefinition { - id: string - version: string - displayName: string - spawnerPrompt: string - model: string - reasoningOptions?: { - enabled: boolean - exclude: boolean - effort: 'low' | 'medium' | 'high' - } - toolNames: string[] - spawnableAgents: string[] - systemPrompt: string - instructionsPrompt?: string - stepPrompt?: string - includeMessageHistory?: boolean - /** Generator function for programmatic orchestration flow */ - handleSteps?: (context: { prompt: string }) => Generator -} diff --git a/.gitignore b/.gitignore index 99845d3..9cd775d 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,4 @@ frontend/.next/ # /frontend/public/faiss_index/ +.agents/ diff --git a/.vscode/settings.json b/.vscode/settings.json index a4f12fa..97ba47e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,6 @@ { - "python.terminal.useEnvFile": true + "python.terminal.useEnvFile": true, + "accessibility.signals.chatUserActionRequired": { + "sound": "on" + } } diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..eb98976 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,408 @@ +# TANGLAW — Scholarship Navigation Portal + +**University**: Polytechnic University of the Philippines (PUP Manila), BSCS 1-2 +**Course**: Science, Technology, and Society (STS) +**Mascot**: Owel (an owl — symbolizing wisdom and guidance) + +AI-powered scholarship navigation portal for Filipino tertiary students. Combines a scholarship directory, readiness assessment, exam reviewer, and AI chatbot companion. + +--- + +## Architecture + +``` +┌──────────────────┐ ┌──────────────────┐ ┌──────────────────────┐ +│ Vercel (Free) │────▶│ Render (Free) │────▶│ Supabase (Free) │ +│ Next.js 16 │ │ Express API │ │ PostgreSQL │ +│ tanglaw.vercel.app│ │ tanglaw-api.onrender.com│ pgvector enabled │ +└──────────────────┘ └──────────────────┘ └──────────────────────┘ +``` + +--- + +## Stack (with exact versions) + +| Layer | Technology | Version | Config File | +|-------|-----------|---------|-------------| +| Frontend | Next.js (App Router) | 16.2.6 | `frontend/next.config.ts` | +| UI | React | 19.2.4 | `frontend/tsconfig.json` | +| Language | TypeScript (strict, ESM) | ^5.9.3 | `frontend/tsconfig.json` (target ES2017, bundler) | +| Backend | Express.js | ^4.18.2 | `backend/tsconfig.json` (CommonJS, target ES2022) | +| ORM | Prisma | 7.8.0 | `backend/prisma/schema.prisma` | +| Database | PostgreSQL (Supabase, Singapore) | — | `render.yaml` env vars | +| Auth (FE) | NextAuth.js v4 (Credentials) | ^4.24.14 | `frontend/src/lib/nextauth.ts` | +| Auth (BE) | JWT (jsonwebtoken + bcryptjs) | ^9.0.0 / ^2.4.3 | `backend/src/middleware/auth.ts` | +| AI (primary) | Google Gemini 3.1 Flash-Lite | — | via `GOOGLE_API_KEY` env var | +| AI (fallback) | OpenRouter free models (6-model cascade) | — | via `OPENROUTER_API_KEY` env var | +| AI framework | LangChain (Core, Google GenAI, OpenAI) | ^1.1.48+ | `backend/src/services/chatService.ts` | +| Styling | Tailwind CSS v4 (CSS custom properties) | ^4 | `frontend/postcss.config.mjs` | +| Animations | Framer Motion | ^11.0.0 | Imported per component | +| Icons | lucide-react | ^0.474.0 | Imported per component | +| Validation | Zod | ^4.4.3 | `frontend/src/lib/backend.ts` types | +| Testing | Vitest (unit) + Playwright (E2E) | ^4.1.8 / ^1.60.0 | `frontend/vitest.config.ts`, `playwright.config.ts` | +| Linting | ESLint | ^9 (FE) / ^8 (BE) | `frontend/eslint.config.mjs`, backend config | +| Package Mgr | npm | — | `package.json` at root | +| Hosting | Vercel (FE free) + Render (BE free) | — | `render.yaml` (Blueprint) + Vercel dashboard | +| Deploy trigger | GitHub push to `main` | — | Auto-deploy on both Vercel + Render | + +--- + +## Critical Rules (must never violate) + +### 1. CommonJS vs ESM + +```typescript +// ✅ BACKEND — CommonJS +// backend/tsconfig.json: "module": "commonjs", "moduleResolution": "node" +import express from 'express' // OK — TypeScript compiles to require() +// ❌ NO top-level await +// ❌ NO ESM-only packages + +// ✅ FRONTEND — ESM +// frontend/tsconfig.json: "module": "esnext", "moduleResolution": "bundler" +import { prisma } from '@/lib/db' // OK — uses @/ alias +import { Scholarship } from '@/components/scholarship-browser' +``` + +### 2. Two Prisma Schemas — Mirror Both + +| File | Role | +|------|------| +| `backend/prisma/schema.prisma` | **Source of truth** — edit this first | +| `frontend/prisma/schema.prisma` | **Mirror** — make identical changes here | + +When editing the Prisma schema, ALWAYS update both files. Then run: +```bash +cd backend && npx prisma generate +cd frontend && npx prisma generate +``` + +### 3. Two Auth Systems — Never Mix + +| System | Technology | Purpose | Token Location | +|--------|-----------|---------|---------------| +| Frontend session | NextAuth.js v4 (CredentialsProvider) | `useSession()`, session cookies | In NextAuth JWT | +| Backend API | Custom JWT (jsonwebtoken) | Authenticate API calls | `localStorage` as key `tanglaw-token`, sent as `Bearer` header | + +Signup/login flow: +1. User submits credentials → NextAuth calls backend `POST /api/auth/login` +2. Backend returns JWT → NextAuth stores JWT in its session token +3. Frontend also stores JWT in `localStorage` as `tanglaw-token` +4. API client (`frontend/src/lib/backend.ts`) reads `tanglaw-token` from localStorage +5. Backend middleware (`backend/src/middleware/auth.ts`) verifies the `Bearer` token + +### 4. Render Cold Starts + +Backend on Render free tier spins down after **15 minutes of inactivity**. First request after idle: +- Cold start takes **30-60 seconds** +- `scholarship-browser.tsx` handles this with error states + retry buttons +- Frontend should show loading/retry UI, not break + +### 5. Path Aliases + +```typescript +// FRONTEND only — @/ maps to frontend/src/ +import { prisma } from '@/lib/db' // resolves: frontend/src/lib/db +import { AuthGuard } from '@/components/AuthGuard' + +// BACKEND only — relative imports +import prisma from '../services/prismaClient' +import { authenticateToken } from '../middleware/auth' +``` + +--- + +## Directory Map + +``` +tanglaw/ +├── CLAUDE.md ← You are here +├── knowledge.md ← MetaBuff knowledge file (project context for AI) +├── package.json ← Root (Playwright + TypeScript dev deps only) +├── playwright.config.ts ← E2E test config (tests in frontend/src/__e2e__/) +├── render.yaml ← Render Blueprint (rootDir: backend, Singapore region) +├── DEPLOY.md ← Full deployment guide +├── DESIGN.md ← Design system (colors, typography, components) +├── PRODUCT.md ← Product vision, brand personality +│ +├── frontend/ ← Next.js 16 App Router (Vercel) +│ ├── package.json ← npm dependencies +│ ├── next.config.ts ← Next.js config (image formats, package optimization) +│ ├── tsconfig.json ← Strict, ES2017, bundler resolution +│ ├── postcss.config.mjs ← Tailwind v4 PostCSS plugin +│ ├── vitest.config.ts ← Vitest config +│ ├── eslint.config.mjs ← ESLint flat config +│ ├── src/ +│ │ ├── app/ ← App Router pages +│ │ │ ├── layout.tsx ← Root layout (fonts, providers, footer) +│ │ │ ├── page.tsx ← Landing page (hero, features, mascot) +│ │ │ ├── globals.css ← CSS custom properties (--theme-*) +│ │ │ ├── (auth)/ +│ │ │ │ ├── login/page.tsx ← Login (NextAuth Credentials form) +│ │ │ │ └── signup/page.tsx ← Signup (name, email, password) +│ │ │ ├── (main)/ +│ │ │ │ ├── scholarships/page.tsx ← Redirects to /dashboard/scholarships +│ │ │ │ └── readiness/page.tsx ← Redirects to /dashboard/readiness +│ │ │ ├── dashboard/ +│ │ │ │ ├── layout.tsx ← AuthGuard + nav + OwelChatbot +│ │ │ │ ├── page.tsx ← Dashboard home (module cards) +│ │ │ │ ├── scholarships/page.tsx ← Scholarship browser +│ │ │ │ ├── readiness/page.tsx ← Readiness assessment quiz +│ │ │ │ └── reviewer/page.tsx ← Exam reviewer +│ │ │ ├── about/page.tsx ← About page +│ │ │ └── contact/page.tsx ← Contact page +│ │ │ └── api/ +│ │ │ ├── chat/route.ts ← LangChain agent executor +│ │ │ └── auth/[...nextauth]/route.ts ← NextAuth API handler +│ │ ├── components/ +│ │ │ ├── AuthGuard.tsx ← Route guard (redirects to /login) +│ │ │ ├── site-header.tsx ← Public site header +│ │ │ ├── NextAuthProvider.tsx ← SessionProvider wrapper +│ │ │ ├── scholarship-browser.tsx ← Scholarship discovery (search, filters, cache) +│ │ │ ├── readiness-form.tsx ← Timed multi-subject quiz +│ │ │ ├── owel-chatbot.tsx ← AI chatbot widget (preloaded prompts) +│ │ │ ├── nature-canvas.tsx ← Background decoration canvas +│ │ │ └── theme-changer.tsx ← Theme toggle +│ │ └── lib/ +│ │ ├── backend.ts ← API client (fetch wrappers, JWT management) +│ │ ├── db.ts ← Prisma client singleton +│ │ ├── nextauth.ts ← NextAuth config (Credentials → backend JWT) +│ │ └── ai/ +│ │ ├── models.ts ← AI model factory (Gemini → OpenRouter) +│ │ ├── prompts.ts ← ChatPromptTemplate (Owel system prompt) +│ │ └── tools.ts ← LangChain tools (searchScholarships, getScholarshipDetails) +│ └── prisma/ +│ └── schema.prisma ← Mirror of backend schema +│ +├── backend/ ← Express API server (Render) +│ ├── package.json ← npm dependencies +│ ├── tsconfig.json ← CommonJS, ES2022, node resolution +│ ├── prisma.config.ts ← Prisma v7 datasource config (DATABASE_URL / DIRECT_URL) +│ ├── start.sh ← Render start: db push + seed + start +│ ├── prisma/ +│ │ ├── schema.prisma ← SOURCE OF TRUTH (Scholarship, Question, User, Message) +│ │ └── seed.ts ← 8 canonical scholarships +│ ├── scripts/ +│ │ ├── test_signup.js ← Manual signup test +│ │ ├── list_scholarships.ts ← Scholarship query script +│ │ └── inspect_user_columns.js ← Schema inspection +│ └── src/ +│ ├── server.ts ← Express entry (CORS, JSON, routes, error handler) +│ ├── routes/index.ts ← All API route definitions +│ ├── controllers/ +│ │ ├── authController.ts ← signup, login, logout, me +│ │ ├── scholarshipController.ts ← getScholarships (filtered, paginated) +│ │ └── chatController.ts ← createMessage, getMessagesForUser +│ ├── middleware/auth.ts ← JWT auth middleware (Bearer token) +│ └── services/ +│ ├── prismaClient.ts ← Prisma singleton (@prisma/adapter-pg + pg.Pool) +│ ├── scholarshipSearchService.ts ← ILIKE search → formatted LLM context +│ └── chatService.ts ← AI pipeline: Gemini → 6 OpenRouter fallbacks +│ +└── .claude/skills/ ← Loaded DAILY skills + ├── frontend-patterns/SKILL.md + ├── backend-patterns/SKILL.md + ├── api-design/SKILL.md + ├── security-review/SKILL.md + ├── documentation-lookup/SKILL.md + ├── nextjs-turbopack/SKILL.md + ├── e2e-testing/SKILL.md + ├── verification-loop/SKILL.md + ├── coding-standards/SKILL.md + ├── strategic-compact/SKILL.md + └── skill-library/SKILL.md ← LIBRARY skill router (off-stack / optional) +``` + +--- + +## API Endpoints + +### Public +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/health` | Health check with uptime | + +### Auth (no token required) +| Method | Path | Body | Response | +|--------|------|------|----------| +| POST | `/api/auth/signup` | `{ name, email, password }` | `{ token, user }` | +| POST | `/api/auth/login` | `{ email, password }` | `{ token, user }` | + +### Auth (token required — `Bearer `) +| Method | Path | Description | +|--------|------|-------------| +| POST | `/api/auth/logout` | End session | +| GET | `/api/auth/me` | Current user info | + +### Protected (auth required) +| Method | Path | Query Params | Description | +|--------|------|-------------|-------------| +| GET | `/api/scholarships` | `?program=§or=&gwa=&page=1&pageSize=10` | Filtered, paginated scholarships | +| POST | `/api/messages` | — | Store chat message | +| GET | `/api/messages` | — | Get current user's messages | + +--- + +## Database Schema (Prisma) + +```prisma +model Scholarship { + id String @id @default(uuid()) + name String + provider String + sector Sector // PUBLIC | PRIVATE + incomeBracket String + programCategories String[] + minGwa Float + requirements String + benefits String + returnService Boolean + link String + contentVector Unsupported("vector")? // pgvector (optional) +} + +model Question { + id String @id @default(uuid()) + type QuestionType // LOGIC | MATH | SCIENCE | ENGLISH | FILIPINO + difficulty Int + text String + choices Json + correctAnswer String + explanation String +} + +model User { + id String @id @default(uuid()) + email String @unique + name String? + passwordHash String? + emailVerified Boolean @default(false) + yearLevel String? + program String? + gwa Float? + financialStatus String? + createdAt DateTime @default(now()) + messages Message[] +} + +model Message { + id String @id @default(uuid()) + role String + content String + metadata Json? + createdAt DateTime @default(now()) + userId String + user User @relation(fields: [userId], references: [id]) +} +``` + +--- + +## Common Scripts + +### Frontend (`cd frontend`) + +| Command | Purpose | +|---------|---------| +| `npm run dev` | Start Next.js dev server (port 3000) | +| `npm run build` | Production build | +| `npm run start` | Start production server | +| `npm run lint` | ESLint check | +| `npm run analyze` | Bundle analyzer | +| `npx vitest run` | Run unit tests | +| `npx tsc --noEmit` | TypeScript typecheck | + +### Backend (`cd backend`) + +| Command | Purpose | +|---------|---------| +| `npm run dev` | Start dev server (ts-node-dev, port 5000) | +| `npm run build` | Compile TypeScript → CommonJS in `dist/` | +| `npm run start` | Start production server | +| `npm run lint` | ESLint check | +| `npm run seed` | Run seed script (8 scholarships) | +| `npx tsc --noEmit` | TypeScript typecheck | +| `npx prisma generate` | Regenerate Prisma client after schema change | +| `npx prisma db push` | Push schema to database (no migration file) | + +### Root + +| Command | Purpose | +|---------|---------| +| `cd frontend && npm run dev` | Start frontend dev server | +| `cd backend && npm run dev` | Start backend dev server | +| `npx playwright test` | Run E2E tests (see playwright.config.ts) | + +--- + +## Environment Variables + +### Frontend (Vercel) + +| Variable | Source | +|----------|--------| +| `NEXT_PUBLIC_BACKEND_URL` | Render URL | +| `NEXT_PUBLIC_SUPABASE_URL` | Supabase project URL | +| `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` | Supabase anon key | +| `NEXTAUTH_SECRET` | Random 32+ char string | +| `DATABASE_URL` | Supabase connection string | + +### Backend (Render) + +| Variable | Source | +|----------|--------| +| `DATABASE_URL` | Supabase pooler (port 6543) | +| `DIRECT_URL` | Supabase direct (port 5432) | +| `JWT_SECRET` | Auto-generated by Render | +| `FRONTEND_URL` | Vercel deployment URL | +| `GOOGLE_API_KEY` | Google AI Studio (for Gemini) | +| `OPENROUTER_API_KEY` | OpenRouter (for fallback LLM) | + +--- + +## Design System + +**Creative North Star**: "The Guiding Beacon" — clarity and hope for students. + +**Colors**: Deep Marine `#1B4079` (primary), Verdant Sage `#CBDF90` (canvas), Morning Mist `#F4F9E2` (surface) + +**Typography**: Outfit (display, black 900) + Inter (body, 400, 1.6 line-height) + +**Corners**: Rounded-xl (24px) for cards, pill-shaped (9999px) for buttons + +See `DESIGN.md` for the full design system specification. + +--- + +## AI/LLM Pipeline + +``` +User message + → LangChain Agent Executor (frontend/src/app/api/chat/route.ts) + → Scholarship tools (searchScholarships, getScholarshipDetails) + → PostgreSQL ILIKE search (scholarshipSearchService.ts) + → Formatted RAG context + → LLM Response + 1st: Google Gemini 3.1 Flash-Lite (GOOGLE_API_KEY) + Fallback: OpenRouter cascade (owl-alpha → nemotron → gpt-oss → llama-3 → qwen-2.5 → gemma-2) +``` + +--- + +## Verification Checklist + +Before committing or deploying: + +``` +[ ] Frontend typecheck: cd frontend && npx tsc --noEmit +[ ] Backend typecheck: cd backend && npx tsc --noEmit +[ ] Frontend lint: cd frontend && npm run lint +[ ] Backend lint: cd backend && npm run lint +[ ] Frontend build: cd frontend && npm run build +[ ] Backend build: cd backend && npm run build +[ ] Frontend tests: cd frontend && npx vitest run +[ ] Prisma schema mirrored to both frontend/prisma/ and backend/prisma/ +[ ] Prisma client regenerated in BOTH: cd backend && npx prisma generate && cd ../frontend && npx prisma generate +[ ] No secrets committed to git +[ ] No console.log left in production code +``` diff --git a/tanglaw/DESIGN.md b/DESIGN.md similarity index 100% rename from tanglaw/DESIGN.md rename to DESIGN.md diff --git a/tanglaw/PRODUCT.md b/PRODUCT.md similarity index 100% rename from tanglaw/PRODUCT.md rename to PRODUCT.md diff --git a/README.md b/README.md index 8cf0ad1..9bc871d 100644 --- a/README.md +++ b/README.md @@ -1,80 +1,156 @@ -# Tanglaw +# TANGLAW -TANGLAW is an AI-powered scholarship navigation portal built with a Next.js frontend and an Express + TypeScript backend. The app combines a secure student dashboard, scholarship discovery tools, readiness assessment modules, and an AI-guided chat companion. +TANGLAW (Tagalog for "light" or "illumination") is an AI-powered scholarship navigation portal designed for Filipino tertiary students. It combines a scholarship directory, readiness assessment, exam reviewer, and an AI chatbot companion into a single, guided dashboard experience. -## Repository structure +--- -- `frontend/` — Next.js app with UI components, AI helpers, Supabase client setup, and dashboard pages. -- `backend/` — Express API service with mock scholarship data, chat persistence endpoints, and Prisma client support. +## Problem -## Getting started +Filipino tertiary students face significant barriers when searching for and applying to scholarships. According to recent data, only 30.5% of Grade 3 learners show basic reading proficiency and just 0.47% of Grade 12 learners demonstrate grade-level readiness. Scholarship research is noisy, fragmented, and difficult to navigate. Many students lack access to verified grant sources, eligibility summaries, and application support tools in one place, leaving them overwhelmed and underserved by existing resources. -### Frontend +--- + +## Solution + +TANGLAW addresses these challenges by providing a centralized, AI-powered platform that simplifies scholarship discovery and preparation. The system is a full-stack application with a Next.js frontend, Express backend, PostgreSQL database, LangChain AI integration, and Supabase hosting. Users can register, browse scholarships with advanced filtering, take readiness assessments, review exam materials, and chat with an AI companion named Owel to guide them through the application journey. The portal focuses on making scholarships easier to find, understand, and act on. + +--- + +## Technologies Used + +| Category | Technology | +|----------|-----------| +| **Frontend** | Next.js 16 (App Router), React 19, TypeScript, Tailwind CSS v4, Framer Motion, lucide-react | +| **Backend** | Express.js, TypeScript (CommonJS), Prisma v7 ORM | +| **Database** | PostgreSQL (Supabase), pgvector for embeddings | +| **AI / LLM** | Google Gemini 3.1 Flash-Lite (primary), OpenRouter free models (fallback cascade), LangChain | +| **Authentication** | NextAuth.js v4 (CredentialsProvider), JWT (jsonwebtoken + bcryptjs) | +| **Testing** | Vitest (unit), Playwright (E2E) | +| **Deployment** | Vercel (frontend), Render (backend), Supabase (database) | +| **Package Manager** | npm | + +--- + +## Key Features + +- **Scholarship Browser** — Search and filter scholarships by income bracket, sector (public/private), and program category with eligibility recommendations +- **Readiness Assessment** — Timed multi-subject quiz covering Math, Science, English, Filipino, and Logic +- **Exam Reviewer** — Review practice questions with explanations and difficulty levels +- **AI Chatbot Companion** — Owel, an AI-powered assistant using RAG (Retrieval-Augmented Generation) to answer scholarship-related queries +- **Secure Student Dashboard** — Protected routes with NextAuth.js authentication and JWT-based API access +- **Responsive UI** — Animated, accessible interface built with Framer Motion and Tailwind CSS + +--- + +## How to Run the Project + +### Prerequisites + +- Node.js 20 or later +- npm +- A Supabase account (for PostgreSQL database) + +### Backend Setup ```bash -cd frontend +cd backend npm install +npx prisma generate +npx prisma db push +npm run seed npm run dev ``` -Open [http://localhost:3000](http://localhost:3000) in your browser. +The backend runs on `http://localhost:5000`. -### Backend +### Frontend Setup ```bash -cd backend +cd frontend npm install npm run dev ``` -Open [http://localhost:4000](http://localhost:4000) if the backend is configured to use port `4000`. +The frontend runs on `http://localhost:3000`. -## Key features +### Optional Checks -- Secure dashboard access using localStorage-based auth guard. -- Scholarship browser with filtering and eligibility-focused recommendations. -- Interactive readiness quiz and exam reviewer tools. -- AI companion integration using LangChain tool calling. -- Simple backend endpoints for health, scholarship mock data, and message persistence. +```bash +# TypeScript typecheck +cd frontend && npx tsc --noEmit -## Frontend pages +# Lint +cd frontend && npm run lint -- `/` — Landing page with product overview and navigation. -- `/about` — Team, goals, and project pillars. -- `/contact` — Contact form and support details. -- `/login` and `/signup` — Authentication entry points. -- `/dashboard` — Authenticated dashboard home. -- `/dashboard/scholarships` — Scholarship search module. -- `/dashboard/readiness` — Readiness assessment module. -- `/dashboard/reviewer` — Exam review module. +# Unit tests +cd frontend && npx vitest run -## Backend endpoints +# E2E tests (requires running dev server) +npx playwright test +``` -- `GET /api/health` — service health check. -- `GET /api/scholarships` — returns mock scholarship metadata. -- `POST /api/messages` — store a chat message in the database. -- `GET /api/messages/:userId` — fetch user messages. +--- -## Deploy and build +## Project Structure -Use standard Next.js and Node scripts to build and deploy each directory independently. +``` +tanglaw/ +├── frontend/ — Next.js 16 App Router (deployed on Vercel) +│ ├── src/app/ — Pages and API routes +│ │ ├── (auth)/ — Login and signup pages +│ │ ├── dashboard/ — Authenticated dashboard (scholarships, readiness, reviewer) +│ │ ├── api/ — Chat API and NextAuth handler +│ │ └── page.tsx — Landing page +│ ├── src/components/ — UI components (scholarship browser, chatbot, quiz) +│ └── src/lib/ — API client, auth config, AI model tools +│ +├── backend/ — Express API (deployed on Render) +│ ├── src/ +│ │ ├── controllers/ — Auth, scholarship, and chat controllers +│ │ ├── middleware/ — JWT authentication middleware +│ │ ├── routes/ — API route definitions +│ │ └── services/ — Prisma client, chat service, scholarship search +│ ├── prisma/ — Database schema (source of truth) and seed data +│ └── start.sh — Render deploy script +│ +├── CLAUDE.md — Project context for AI assistants +├── DESIGN.md — Design system specification +├── PRODUCT.md — Product vision and brand personality +├── DEPLOY.md — Full deployment guide (Vercel + Render + Supabase) +└── render.yaml — Render Blueprint configuration +``` -### Frontend build +--- -```bash -cd frontend -npm run build -npm run start -``` +## License -### Backend build +This project is intended for academic and project submission use. -```bash -cd backend -npm run build -npm start -``` +--- + +## Documentation & Development Team + +### Documentation Team + +| Name | Role | +|------|------| +| Godsent John C. Salvaloza | Documentation Head | +| Rhaine Venice B. Bonador | Introduction Writer | +| Kyle Ashley B. Madera | Statement of the Problem Writer | +| Hannah Mae V. Alberto | RRL Lead Writer | +| Hannah Nicole B. Partible | RRL Assistant & Citation Checker | +| Emerald T. Perez | Methodology Writer | +| Julliane Mae G. Araullo | Results Writer | +| Daniel F. Pajares | Discussion Writer | + +### Development Team -## Notes +| Name | Role | +|------|------| +| Bennett P. Payoyo | Project Manager | +| An-joe Mikael T. Albano | Frontend Developer | +| Levrone Viel S. Delos Reyes | Frontend & QA | +| Charles Joseph V. Faustino | Backend Developer & Database Manager | +| Justin Angelo G. Cruz | QA Tester / Technical Documentation | -The frontend currently uses simulated auth state in local storage and integrates with AI tools for advanced scholarship search and detail retrieval. +**Institution:** Polytechnic University of the Philippines (PUP Manila) — BSCS 1-2, Science, Technology, and Society (STS) diff --git a/backend/package-lock.json b/backend/package-lock.json index 2c1c455..092ea7c 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -35,7 +35,7 @@ "eslint": "^8.0.0", "prisma": "^7.8.0", "ts-node-dev": "^2.0.0", - "typescript": "^5.5.4" + "typescript": "^5.9.3" } }, "node_modules/@cfworker/json-schema": { diff --git a/backend/package.json b/backend/package.json index 45f4db4..4145ed3 100644 --- a/backend/package.json +++ b/backend/package.json @@ -9,6 +9,7 @@ "start": "node dist/server.js", "lint": "eslint . --ext .ts", "seed": "npx ts-node --transpile-only prisma/seed.ts", + "seed:questions": "npx ts-node --transpile-only prisma/seed-questions.ts", "postinstall": "npx prisma generate" }, "dependencies": { @@ -38,6 +39,6 @@ "eslint": "^8.0.0", "prisma": "^7.8.0", "ts-node-dev": "^2.0.0", - "typescript": "^5.5.4" + "typescript": "^5.9.3" } } diff --git a/tanglaw/backend/prisma/init.sql b/backend/prisma/init.sql similarity index 85% rename from tanglaw/backend/prisma/init.sql rename to backend/prisma/init.sql index dbb216d..885eae5 100644 --- a/tanglaw/backend/prisma/init.sql +++ b/backend/prisma/init.sql @@ -1,14 +1,12 @@ -Loaded Prisma config from prisma.config.ts. - --- CreateSchema -CREATE SCHEMA IF NOT EXISTS "public"; - -- CreateEnum CREATE TYPE "Sector" AS ENUM ('PUBLIC', 'PRIVATE'); -- CreateEnum CREATE TYPE "QuestionType" AS ENUM ('LOGIC', 'MATH', 'SCIENCE', 'ENGLISH', 'FILIPINO'); +-- CreateEnum +CREATE TYPE "AssessmentMode" AS ENUM ('DIAGNOSTIC', 'MOCK'); + -- CreateTable CREATE TABLE "Scholarship" ( "id" TEXT NOT NULL, @@ -32,6 +30,10 @@ CREATE TABLE "Question" ( "id" TEXT NOT NULL, "type" "QuestionType" NOT NULL, "difficulty" INTEGER NOT NULL, + "assessmentMode" "AssessmentMode" NOT NULL DEFAULT 'DIAGNOSTIC', + "sourceLabel" TEXT, + "sequenceNo" INTEGER NOT NULL DEFAULT 0, + "isActive" BOOLEAN NOT NULL DEFAULT true, "text" TEXT NOT NULL, "choices" JSONB NOT NULL, "correctAnswer" TEXT NOT NULL, @@ -42,7 +44,7 @@ CREATE TABLE "Question" ( -- CreateTable CREATE TABLE "User" ( - "id" TEXT NOT NULL, + "id" TEXT NOT NULL DEFAULT gen_random_uuid(), "email" TEXT NOT NULL, "name" TEXT, "passwordHash" TEXT, @@ -73,4 +75,3 @@ CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); -- AddForeignKey ALTER TABLE "Message" ADD CONSTRAINT "Message_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index da57c1d..355fe5d 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -22,13 +22,17 @@ model Scholarship { } model Question { - id String @id @default(uuid()) - type QuestionType - difficulty Int - text String - choices Json - correctAnswer String - explanation String + id String @id @default(uuid()) + type QuestionType + difficulty Int + assessmentMode AssessmentMode @default(DIAGNOSTIC) + sourceLabel String? + sequenceNo Int @default(0) + isActive Boolean @default(true) + text String + choices Json + correctAnswer String + explanation String } model User { @@ -67,3 +71,8 @@ enum QuestionType { ENGLISH FILIPINO } + +enum AssessmentMode { + DIAGNOSTIC + MOCK +} diff --git a/backend/prisma/seed-questions.ts b/backend/prisma/seed-questions.ts new file mode 100644 index 0000000..3768d82 --- /dev/null +++ b/backend/prisma/seed-questions.ts @@ -0,0 +1,51 @@ +import { AssessmentMode, Prisma, QuestionType } from "@prisma/client"; +import prisma from "../src/services/prismaClient"; +import { ParsedQuestion, parseOption1, parseOption2 } from "../scripts/parse_question_bank"; + +function toRows( + questions: ParsedQuestion[], + assessmentMode: AssessmentMode, + sourceLabel: string +): Prisma.QuestionCreateManyInput[] { + const sequenceBySubject = new Map(); + + return questions.map((q) => { + const sequenceNo = sequenceBySubject.get(q.subject) ?? 0; + sequenceBySubject.set(q.subject, sequenceNo + 1); + + return { + type: q.subject as QuestionType, + difficulty: q.difficulty, + assessmentMode, + sourceLabel, + sequenceNo, + isActive: true, + text: q.text, + choices: q.choices, + correctAnswer: String(q.correctAnswer), + explanation: q.explanation, + }; + }); +} + +async function main() { + const diagnostic = toRows(parseOption1(), "DIAGNOSTIC", "option1.md"); + const mock = toRows(parseOption2(), "MOCK", "option2.md"); + const rows = [...diagnostic, ...mock]; + + console.log(`Seeding ${rows.length} questions (${diagnostic.length} diagnostic, ${mock.length} mock)...`); + + await prisma.question.deleteMany(); + await prisma.question.createMany({ data: rows }); + + console.log(`✅ Seeded ${rows.length} question records successfully.`); +} + +main() + .catch((error) => { + console.error("❌ Seed failed:", error); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/backend/prisma/seed.ts b/backend/prisma/seed.ts index 45bf901..4289d8e 100644 --- a/backend/prisma/seed.ts +++ b/backend/prisma/seed.ts @@ -1,5 +1,6 @@ import { Sector } from "@prisma/client"; import prisma from "../src/services/prismaClient"; +import { SCHOLARSHIPS_DATA } from "../../frontend/src/data/canonical-scholarships"; interface ScholarshipSeed { name: string; @@ -14,105 +15,31 @@ interface ScholarshipSeed { link: string; } +const extractIncomeBracket = (value?: string): number => { + const match = value?.match(/\b(?:Php|₱)?\s*([0-9]{1,3}(?:,[0-9]{3})*(?:\.\d+)?)/i); + return match ? Number(match[1].replace(/,/g, "")) : 0; +}; + +const extractMinGwa = (value?: string): number => { + const match = value?.match(/(\d+(?:\.\d+)?)%/); + return match ? Number(match[1]) : 0; +}; + +const toSector = (classification: string): Sector => (/national government|government/i.test(classification) ? "PUBLIC" : "PRIVATE"); + async function main() { - const scholarships: ScholarshipSeed[] = [ - { - name: "DOST-SEI Undergraduate Scholarship", - provider: "Department of Science and Technology", - sector: "PUBLIC", - incomeBracket: 0, - programCategories: ["STEM"], - minGwa: 85, - requirements: "Natural-born Filipino citizen\nGWA of 85% or higher\nBelongs to STEM strand in high school (or top 5% of non-STEM class)\nMust pass the DOST-SEI exam", - benefits: "Full Tuition & school fees coverage (up to ₱40,000/yr)\nMonthly Living Allowance (₱7,000/month)\nBook & transportation subsidies\nGroup health insurance", - returnService: true, - link: "https://www.sei.dost.gov.ph", - }, - { - name: "CHED Merit Scholarship Program (CMSP)", - provider: "Commission on Higher Education", - sector: "PUBLIC", - incomeBracket: 400000, - programCategories: ["Any"], - minGwa: 90, - requirements: "Filipino citizen\nCombined family income of ₱400,000 or below\nGeneral Weighted Average (GWA) of 90% or above", - benefits: "Full Tuition subsidy (up to ₱120,000/yr for private; free in SUCs)\nStipend of ₱80,000 per academic year\nBook and study grant allowance", - returnService: false, - link: "https://ched.gov.ph", - }, - { - name: "SM Foundation College Scholarship", - provider: "SM Foundation", - sector: "PRIVATE", - incomeBracket: 250000, - programCategories: ["STEM"], - minGwa: 88, - requirements: "Graduate of public high schools or SM-partner private schools\nAnnual family income not exceeding ₱250,000\nGeneral Weighted Average (GWA) of 88% or above in Grade 12", - benefits: "Full Tuition & matriculation coverage\nMonthly living stipend\nExclusive part-time job opportunities during breaks\nAssured placement in SM Group of Companies after graduation", - returnService: false, - link: "https://www.sm-foundation.org", - }, - { - name: "Manila City Educational Assistance", - provider: "City Government of Manila", - sector: "PUBLIC", - incomeBracket: 200000, - programCategories: ["Any"], - minGwa: 0, - requirements: "Resident of Manila City for at least 3 years\nEnrolled in state colleges/universities (SUCs) or local colleges\nParent must be a registered voter in Manila", - benefits: "₱5,000 educational cash aid per semester\nPriority in local government internship positions", - returnService: false, - link: "https://manila.gov.ph", - }, - { - name: "Mega-Tech Computer Science Scholarship", - provider: "Mega-Tech Group Philippines", - sector: "PRIVATE", - incomeBracket: 0, - programCategories: ["STEM"], - minGwa: 0, - requirements: "Incoming 1st year BSCS, BSIT, or BSCpE student\nMust maintain a semester GWA of 1.75 or better\nActive portfolio showing mini coding projects is highly prioritized", - benefits: "100% Tuition & miscellaneous fees covered\nTech-pack allowance (high-spec laptop and accessories)\nGuaranteed internship and 2-year employment contract after college", - returnService: true, - link: "https://megatech-grants.org", - }, - { - name: "Health-Care Alliance Foundation Grant", - provider: "Health-Care Alliance PH", - sector: "PRIVATE", - incomeBracket: 300000, - programCategories: ["Medical-Allied"], - minGwa: 0, - requirements: "Currently enrolled in Nursing, MedTech, or Pharmacy program\nAnnual household income below ₱300,000\nMaintain a GPA of 2.25 or higher without failing grades", - benefits: "₱35,000 financial subsidy per semester\nClinical clerkship stipend and uniform allowances\nFree reviewer materials for board exams", - returnService: false, - link: "https://healthcare-alliance.org", - }, - { - name: "Humanities & Arts Excellence Fellowship", - provider: "Cultural Center Sponsoring Board", - sector: "PRIVATE", - incomeBracket: 0, - programCategories: ["Humanities"], - minGwa: 0, - requirements: "Enrolled in Literature, Fine Arts, History, or Philosophy programs\nSubmit a portfolio of 3 original essays or artistic drafts\nRecommendation letter from the Department Chair", - benefits: "₱40,000 subsidy per school year\nFully sponsored publication and thesis printing grants\nFree admission to writing conventions and artistic forums", - returnService: false, - link: "https://humanities-fellows.ph", - }, - { - name: "Tulong Dunong Program (TDP-TES)", - provider: "UniFAST & CHED", - sector: "PUBLIC", - incomeBracket: 300000, - programCategories: ["Any"], - minGwa: 0, - requirements: "Filipino tertiary student enrolled in CHED-recognized SUCs or LUCs\nNo other major active government educational scholarship\nPassing grades in all subjects", - benefits: "₱15,000 financial assistance per school year\nCan be combined with local government subsidies", - returnService: false, - link: "https://unifast.deped.gov.ph", - }, - ]; + const scholarships: ScholarshipSeed[] = SCHOLARSHIPS_DATA.map((item) => ({ + name: item.name, + provider: item.provider, + sector: toSector(item.classification), + incomeBracket: extractIncomeBracket(item.eligibility.financialStatus ?? item.coverageDetails ?? item.overview), + programCategories: item.priorityPrograms.length ? item.priorityPrograms : ["Any"], + minGwa: extractMinGwa(item.eligibility.minimumGPA), + requirements: item.requirements.join("\n"), + benefits: item.coverageDetails || item.overview, + returnService: /return service|commitment/i.test(`${item.coverageDetails} ${item.overview}`), + link: item.links[0] ?? "", + })); console.log(`Seeding ${scholarships.length} scholarships...`); diff --git a/backend/scripts/check_seed.ts b/backend/scripts/check_seed.ts new file mode 100644 index 0000000..9165aa6 --- /dev/null +++ b/backend/scripts/check_seed.ts @@ -0,0 +1,21 @@ +import prisma from "../src/services/prismaClient"; + +async function main() { + const count = await prisma.scholarship.count(); + const rows = await prisma.scholarship.findMany({ + take: 5, + select: { name: true, provider: true, sector: true }, + }); + + console.log(`COUNT=${count}`); + console.log(`SAMPLE=${JSON.stringify(rows)}`); +} + +main() + .catch((error) => { + console.error(error); + process.exitCode = 1; + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/backend/scripts/parse_question_bank.ts b/backend/scripts/parse_question_bank.ts new file mode 100644 index 0000000..e665b4c --- /dev/null +++ b/backend/scripts/parse_question_bank.ts @@ -0,0 +1,270 @@ +import fs from "fs"; +import path from "path"; + +export type Subject = "MATH" | "ENGLISH" | "FILIPINO" | "SCIENCE" | "LOGIC"; + +export interface ParsedQuestion { + subject: Subject; + difficulty: number; + text: string; + choices: string[]; + correctAnswer: number; + explanation: string; +} + +interface SubjectRange { + subject: Subject; + start: number; + end: number; + /** Set for sections whose questions have no numbering and run directly into their options. */ + unnumbered?: boolean; +} + +const OPTION1_PATH = path.join(__dirname, "../../frontend/src/components/option1.md"); +const OPTION2_PATH = path.join(__dirname, "../../frontend/src/components/option2.md"); + +const OPTION1_RANGES: SubjectRange[] = [ + { subject: "MATH", start: 1, end: 375 }, + { subject: "ENGLISH", start: 376, end: 749 }, + { subject: "FILIPINO", start: 750, end: 1362 }, + { subject: "SCIENCE", start: 1363, end: 1880 }, + { subject: "LOGIC", start: 1881, end: 2300 }, +]; + +const OPTION2_RANGES: SubjectRange[] = [ + { subject: "MATH", start: 1, end: 394 }, + { subject: "ENGLISH", start: 395, end: 796, unnumbered: true }, + { subject: "FILIPINO", start: 797, end: 1758 }, + { subject: "SCIENCE", start: 1759, end: 2316 }, + { subject: "LOGIC", start: 2317, end: 2826 }, +]; + +const DIFFICULTY_RE = /(?:tier|difficulty)\s*(?:level)?\s*#?\s*:?\s*(\d)/i; +const ANSWER_LINE_RE = /^(?:answer|sagot|tamang sagot)\s*:\s*(.+)$/i; +const LETTER_ANSWER_RE = /^([a-e])\b/i; +const OPTION_RE = /^([a-e])[.)]\s*(.*)$/i; +const QUESTION_HASH_TEXT_RE = /^question\s*#?\s*\d+\s*:?\s*(.*)$/i; +const HASH_ONLY_RE = /^#\s*\d+\s*$/; +const NUMBERED_RE = /^(?:q\s*)?\d{1,3}[.)]\s*(.*)$/i; +const EXPLANATION_RE = /^(?:short\s+)?explanation\s*:?\s*(.*)$/i; +const OPTION_A_RE = /^a[.)]/i; + +function isQuestionStart(line: string): boolean { + return QUESTION_HASH_TEXT_RE.test(line) || HASH_ONLY_RE.test(line) || NUMBERED_RE.test(line); +} + +function getLeadingText(line: string): string { + const hashMatch = line.match(QUESTION_HASH_TEXT_RE); + if (hashMatch) return hashMatch[1].trim(); + const numberedMatch = line.match(NUMBERED_RE); + if (numberedMatch) return numberedMatch[1].trim(); + return ""; +} + +function parseSubjectBlock(rawLines: string[], subject: Subject, unnumbered = false): ParsedQuestion[] { + const lines = rawLines.map((l) => l.trim()); + const questions: ParsedQuestion[] = []; + let currentDifficulty = 1; + let pos = 0; + + // Skip front-matter/preamble lines until the first question marker. + if (unnumbered) { + // No numbering to detect a question start; look ahead for the first + // "A." option line and treat the line before it as the first question. + while (pos < lines.length) { + const d = lines[pos].match(DIFFICULTY_RE); + if (d) currentDifficulty = Number(d[1]); + if (lines[pos] && OPTION_A_RE.test(lines[pos + 1] ?? "")) break; + pos++; + } + } else { + while (pos < lines.length && !isQuestionStart(lines[pos])) { + const d = lines[pos].match(DIFFICULTY_RE); + if (d) currentDifficulty = Number(d[1]); + pos++; + } + } + + while (pos < lines.length) { + const line = lines[pos]; + if (!line) { + pos++; + continue; + } + + const d = line.match(DIFFICULTY_RE); + if (d && !isQuestionStart(line)) { + currentDifficulty = Number(d[1]); + pos++; + continue; + } + + if (!isQuestionStart(line) && !unnumbered) { + pos++; + continue; + } + + const questionTextParts: string[] = []; + const leading = isQuestionStart(line) ? getLeadingText(line) : line; + if (leading) questionTextParts.push(leading); + pos++; + + const optionLines: { letter: string; text: string }[] = []; + + while (pos < lines.length && !ANSWER_LINE_RE.test(lines[pos])) { + const l = lines[pos]; + if (!l) { + pos++; + continue; + } + const opt = l.match(OPTION_RE); + if (opt) { + optionLines.push({ letter: opt[1].toLowerCase(), text: opt[2].trim() }); + } else if (optionLines.length === 0) { + questionTextParts.push(l); + } else { + optionLines[optionLines.length - 1].text += ` ${l}`; + } + pos++; + } + + if (pos >= lines.length) break; // dangling question with no answer key — drop it + + const answerMatch = lines[pos].match(ANSWER_LINE_RE); + const answerRaw = answerMatch![1].trim(); + const letterMatch = answerRaw.match(LETTER_ANSWER_RE); + const answerLetter = letterMatch ? letterMatch[1].toLowerCase() : null; + pos++; + + const explanationParts: string[] = []; + while (pos < lines.length) { + const l = lines[pos]; + if (!l) { + pos++; + if (explanationParts.length > 0) break; + continue; + } + if (isQuestionStart(l) || ANSWER_LINE_RE.test(l) || OPTION_RE.test(l) || DIFFICULTY_RE.test(l)) break; + const exp = l.match(EXPLANATION_RE); + if (exp) { + explanationParts.push(exp[1].trim()); + pos++; + if (unnumbered) break; // unnumbered sections only ever have a single explanation line + continue; + } + if (explanationParts.length > 0 && !unnumbered) { + explanationParts.push(l); + pos++; + continue; + } + break; + } + + let choices: string[]; + let correctAnswer: number; + + if (optionLines.length >= 2) { + choices = optionLines.map((o) => o.text).filter(Boolean); + if (answerLetter) { + const idx = optionLines.findIndex((o) => o.letter === answerLetter); + correctAnswer = idx >= 0 ? idx : answerLetter.charCodeAt(0) - 97; + } else { + correctAnswer = choices.findIndex((c) => c.toLowerCase() === answerRaw.toLowerCase()); + } + } else { + const extra = Math.max(0, questionTextParts.length - 1); + let take = Math.min(4, extra); + if (extra === 5) { + const last5 = questionTextParts.slice(-5); + if (last5.every((l) => l.length <= 20)) take = 5; + } + choices = questionTextParts.splice(questionTextParts.length - take, take); + if (answerLetter) { + correctAnswer = answerLetter.charCodeAt(0) - 97; + } else { + correctAnswer = choices.findIndex((c) => c.toLowerCase() === answerRaw.toLowerCase()); + } + } + + const text = questionTextParts.filter(Boolean).join(" ").trim(); + const explanation = explanationParts.filter(Boolean).join(" ").trim(); + + if (!text || choices.length < 2 || correctAnswer < 0 || correctAnswer >= choices.length) { + continue; + } + + questions.push({ subject, difficulty: currentDifficulty, text, choices, correctAnswer, explanation }); + } + + return questions; +} + +function parseFile(filePath: string, ranges: SubjectRange[]): ParsedQuestion[] { + const content = fs.readFileSync(filePath, "utf-8"); + const allLines = content.split(/\r?\n/); + const results: ParsedQuestion[] = []; + + for (const range of ranges) { + const blockLines = allLines.slice(range.start - 1, range.end); + results.push(...parseSubjectBlock(blockLines, range.subject, range.unnumbered)); + } + + return results; +} + +/** option1.md → DIAGNOSTIC pool. Returns all parsed questions, ~50/subject. */ +export function parseOption1(): ParsedQuestion[] { + return parseFile(OPTION1_PATH, OPTION1_RANGES); +} + +/** + * option2.md → MOCK pool. Per subject, keeps only the first 50 valid + * parsed questions (in document order) and warns if a subject falls short. + */ +export function parseOption2(): ParsedQuestion[] { + const all = parseFile(OPTION2_PATH, OPTION2_RANGES); + const bySubject = new Map(); + for (const q of all) { + const list = bySubject.get(q.subject) ?? []; + list.push(q); + bySubject.set(q.subject, list); + } + + const result: ParsedQuestion[] = []; + for (const range of OPTION2_RANGES) { + const list = bySubject.get(range.subject) ?? []; + if (list.length < 50) { + console.warn(`[option2.md] ${range.subject}: only ${list.length}/50 questions parsed`); + } + result.push(...list.slice(0, 50)); + } + return result; +} + +if (require.main === module) { + for (const [label, parse] of [ + ["option1.md (DIAGNOSTIC)", parseOption1], + ["option2.md (MOCK)", parseOption2], + ] as const) { + const questions = parse(); + console.log(`\n=== ${label}: ${questions.length} total ===`); + + const bySubject = new Map(); + for (const q of questions) { + const list = bySubject.get(q.subject) ?? []; + list.push(q); + bySubject.set(q.subject, list); + } + + for (const [subject, list] of bySubject) { + const byDifficulty = new Map(); + for (const q of list) { + byDifficulty.set(q.difficulty, (byDifficulty.get(q.difficulty) ?? 0) + 1); + } + const diffSummary = [1, 2, 3, 4, 5] + .map((d) => `d${d}=${byDifficulty.get(d) ?? 0}`) + .join(" "); + console.log(` ${subject}: ${list.length} (${diffSummary})`); + } + } +} diff --git a/backend/src/controllers/authController.ts b/backend/src/controllers/authController.ts index bde96ff..dd6e95f 100644 --- a/backend/src/controllers/authController.ts +++ b/backend/src/controllers/authController.ts @@ -1,15 +1,9 @@ import { Request, Response } from "express"; import bcrypt from "bcryptjs"; import jwt from "jsonwebtoken"; -import prisma from "../services/prismaClient"; - -const JWT_SECRET = process.env.JWT_SECRET; -const JWT_EXPIRES_IN = "2h"; - -if (!JWT_SECRET) { - throw new Error("JWT_SECRET environment variable is required"); -} +import { createUserRecord, getUserByEmail } from "../services/supabaseUserDb"; +const JWT_SECRET = process.env.JWT_SECRET ?? "dev-jwt-secret-change-me"; type AuthenticatedUser = { id: string; email: string; @@ -17,9 +11,7 @@ type AuthenticatedUser = { }; const createToken = (user: AuthenticatedUser) => { - return jwt.sign({ userId: user.id, email: user.email, name: user.name }, JWT_SECRET, { - expiresIn: JWT_EXPIRES_IN, - }); + return jwt.sign({ userId: user.id, email: user.email, name: user.name }, JWT_SECRET); }; const isValidEmail = (value: unknown): value is string => { @@ -40,21 +32,17 @@ export const signup = async (req: Request, res: Response) => { } try { - const existing = await prisma.user.findUnique({ where: { email } }); + const existing = await getUserByEmail(email); if (existing) { return res.status(409).json({ error: "A user with this email already exists." }); } const passwordHash = await bcrypt.hash(password, 10); - const user = await prisma.user.create({ - data: { - email, - name: fullName, - passwordHash, - emailVerified: false, - }, - }); + const user = await createUserRecord({ email, name: fullName, passwordHash }); + if (!user) { + return res.status(500).json({ error: "Unable to create account." }); + } const token = createToken(user); @@ -81,7 +69,7 @@ export const login = async (req: Request, res: Response) => { } try { - const user = await prisma.user.findUnique({ where: { email } }); + const user = await getUserByEmail(email); if (!user || !user.passwordHash) { return res.status(401).json({ error: "Invalid credentials." }); } diff --git a/backend/src/controllers/chatController.ts b/backend/src/controllers/chatController.ts index 66aab90..3fca2f8 100644 --- a/backend/src/controllers/chatController.ts +++ b/backend/src/controllers/chatController.ts @@ -36,10 +36,17 @@ export const createMessage = async (req: Request, res: Response) => { } }; +/** Daily AI chat limit per user (free tier protection). */ +const DAILY_AI_LIMIT = 3; + /** * AI-powered chat endpoint that uses the RAG pipeline * (Gemini 3.1 Flash-Lite → OpenRouter fallbacks) to generate * a contextual response based on the user's question. + * + * Enforces a daily limit of 3 AI-generated messages per user + * to protect free tier API rate limits. Preloaded/hardcoded + * quick questions on the frontend are NOT counted here. */ export const chatWithOwel = async (req: Request, res: Response) => { const authReq = req as AuthenticatedRequest; @@ -55,9 +62,36 @@ export const chatWithOwel = async (req: Request, res: Response) => { } try { + // ── Daily usage limit (3 AI queries per day) ──────────────────────── + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const aiMessageCount = await prisma.message.count({ + where: { + userId: user.id, + createdAt: { gte: today }, + metadata: { path: ["source"], equals: "ai-rag" }, + }, + }); + + if (aiMessageCount >= DAILY_AI_LIMIT) { + console.log(`[chatWithOwel] Daily limit reached for user ${user.id} (${aiMessageCount}/${DAILY_AI_LIMIT})`); + return res.json({ + answer: `Hoot! You've reached your daily AI chat limit of ${DAILY_AI_LIMIT} queries. This helps us keep TANGLAW free for everyone. You can still use the Quick Questions above, or come back tomorrow — I'll be here! 🦉`, + code: "DAILY_LIMIT", + remaining: 0, + limit: DAILY_AI_LIMIT, + }); + } + // Use the user's ID as the session key so conversation history persists const answer = await generateChatResponse(question.trim(), user.id); - res.json({ answer }); + + res.json({ + answer, + remaining: DAILY_AI_LIMIT - (aiMessageCount + 1), + limit: DAILY_AI_LIMIT, + }); } catch (err) { console.error("[chatWithOwel] Error:", err); res.status(500).json({ diff --git a/backend/src/controllers/questionController.ts b/backend/src/controllers/questionController.ts new file mode 100644 index 0000000..6798815 --- /dev/null +++ b/backend/src/controllers/questionController.ts @@ -0,0 +1,101 @@ +import { Request, Response } from "express"; +import { Prisma, Question, QuestionType } from "@prisma/client"; +import prisma from "../services/prismaClient"; + +const SUBJECT_LABELS: Record = { + MATH: "Mathematics", + SCIENCE: "Science", + ENGLISH: "English", + FILIPINO: "Filipino", + LOGIC: "Logical Reasoning", +}; + +// Order the mock exam's subject blocks follow (matches frontend SUBJECTS order). +const SUBJECT_ORDER: QuestionType[] = ["MATH", "SCIENCE", "ENGLISH", "FILIPINO", "LOGIC"]; + +const parseSubjectsQuery = (value: unknown): QuestionType[] => { + if (typeof value !== "string") return []; + return value + .split(",") + .map((v) => v.trim().toUpperCase()) + .filter((v): v is QuestionType => SUBJECT_ORDER.includes(v as QuestionType)); +}; + +const parseDifficultyQuery = (value: unknown): number[] => { + if (typeof value !== "string") return []; + return value + .split(",") + .map((v) => Number(v.trim())) + .filter((v) => Number.isInteger(v) && v >= 1 && v <= 5); +}; + +const shuffle = (items: T[]): T[] => { + const result = [...items]; + for (let i = result.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [result[i], result[j]] = [result[j], result[i]]; + } + return result; +}; + +const formatQuestion = (row: Question, id: number) => ({ + id, + subject: SUBJECT_LABELS[row.type], + difficulty: row.difficulty, + questionText: row.text, + options: row.choices as string[], + correctAnswer: Number(row.correctAnswer), +}); + +/** + * Controller for the readiness assessment question bank. + * `mode=diagnostic` returns a shuffled, filtered subset of the DIAGNOSTIC pool. + * `mode=mock` returns the full MOCK pool, grouped by subject in SUBJECT_ORDER. + */ +export const getQuestions = async (req: Request, res: Response) => { + try { + const mode = typeof req.query.mode === "string" && req.query.mode.toLowerCase() === "mock" ? "MOCK" : "DIAGNOSTIC"; + + if (mode === "MOCK") { + const rows = await prisma.question.findMany({ + where: { assessmentMode: "MOCK", isActive: true }, + orderBy: { sequenceNo: "asc" }, + }); + + const bySubject = new Map(); + for (const row of rows) { + const list = bySubject.get(row.type) ?? []; + list.push(row); + bySubject.set(row.type, list); + } + + const ordered = SUBJECT_ORDER.flatMap((subject) => bySubject.get(subject) ?? []); + return res.json({ data: ordered.map((row, idx) => formatQuestion(row, idx + 1)) }); + } + + const subjects = parseSubjectsQuery(req.query.subjects); + const difficulties = parseDifficultyQuery(req.query.difficulty); + const count = Math.min(Math.max(Number(req.query.count) || 10, 1), 50); + + const where: Prisma.QuestionWhereInput = { + assessmentMode: "DIAGNOSTIC", + isActive: true, + type: { in: subjects.length ? subjects : SUBJECT_ORDER }, + }; + + let pool = await prisma.question.findMany({ + where: difficulties.length ? { ...where, difficulty: { in: difficulties } } : where, + }); + + if (pool.length === 0 && difficulties.length) { + pool = await prisma.question.findMany({ where }); + } + + const selected = shuffle(pool).slice(0, Math.min(count, pool.length)); + res.json({ data: selected.map((row, idx) => formatQuestion(row, idx + 1)) }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + console.error("[getQuestions] Database query failed:", message, err instanceof Error ? err.stack : ""); + res.status(500).json({ error: `Database query failed: ${message}` }); + } +}; diff --git a/backend/src/controllers/scholarshipController.ts b/backend/src/controllers/scholarshipController.ts index e7376b5..2dfb3ec 100644 --- a/backend/src/controllers/scholarshipController.ts +++ b/backend/src/controllers/scholarshipController.ts @@ -23,7 +23,7 @@ export const getScholarships = async (req: Request, res: Response) => { const sector = parseStringQuery(req.query.sector)?.toUpperCase(); const gwa = parseNumberQuery(req.query.gwa, NaN); const page = Math.max(parseNumberQuery(req.query.page, 1), 1); - const pageSize = Math.min(Math.max(parseNumberQuery(req.query.pageSize, 20), 1), 50); + const pageSize = Math.min(Math.max(parseNumberQuery(req.query.pageSize, 100), 1), 500); const where: Prisma.ScholarshipWhereInput = {}; if (program) { @@ -42,6 +42,7 @@ export const getScholarships = async (req: Request, res: Response) => { take: pageSize, select: { id: true, + minGwa: true, name: true, provider: true, sector: true, @@ -58,8 +59,9 @@ export const getScholarships = async (req: Request, res: Response) => { name: scholarship.name, provider: scholarship.provider, type: scholarship.sector === "PUBLIC" ? "Public" : "Private", + minGwa: scholarship.minGwa, incomeBracket: Number(scholarship.incomeBracket), - program: scholarship.programCategories?.length ? scholarship.programCategories[0] : "Any", + programCategories: scholarship.programCategories || [], benefits: scholarship.benefits .split(/\r?\n/) .map((item) => item.trim()) @@ -73,7 +75,8 @@ export const getScholarships = async (req: Request, res: Response) => { res.json({ data: formatted }); } catch (err) { - console.error(err); - res.status(500).json({ error: "DB error" }); + const message = err instanceof Error ? err.message : String(err); + console.error("[getScholarships] Database query failed:", message, err instanceof Error ? err.stack : ""); + res.status(500).json({ error: `Database query failed: ${message}` }); } }; diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts index ec6eec8..f0eddf3 100644 --- a/backend/src/middleware/auth.ts +++ b/backend/src/middleware/auth.ts @@ -1,11 +1,8 @@ import { NextFunction, Request, Response } from "express"; import jwt from "jsonwebtoken"; -import prisma from "../services/prismaClient"; +import { getUserById } from "../services/supabaseUserDb"; -const JWT_SECRET = process.env.JWT_SECRET; -if (!JWT_SECRET) { - throw new Error("JWT_SECRET environment variable is required"); -} +const JWT_SECRET = process.env.JWT_SECRET ?? "dev-jwt-secret-change-me"; type JwtPayload = { userId: string; @@ -31,7 +28,7 @@ export const authenticateToken = async (req: Request, res: Response, next: NextF try { const payload = jwt.verify(token, JWT_SECRET) as JwtPayload; - const user = await prisma.user.findUnique({ where: { id: payload.userId } }); + const user = await getUserById(payload.userId); if (!user) { return res.status(401).json({ error: "Invalid token user" }); diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index 629a11c..1cc9317 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -1,5 +1,6 @@ import { Router } from "express"; import { getScholarships } from "../controllers/scholarshipController"; +import { getQuestions } from "../controllers/questionController"; import { createMessage, getMessagesForUser, chatWithOwel } from "../controllers/chatController"; import { signup, login, me, logout } from "../controllers/authController"; import { authenticateToken } from "../middleware/auth"; @@ -21,6 +22,7 @@ router.post("/auth/logout", authenticateToken, logout); router.get("/auth/me", authenticateToken, me); router.get("/scholarships", authenticateToken, getScholarships); +router.get("/questions", authenticateToken, getQuestions); // AI chat endpoint router.post("/chat", authenticateToken, chatWithOwel); diff --git a/backend/src/server.ts b/backend/src/server.ts index 19c7ad1..5a3b85b 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -9,15 +9,13 @@ import apiRouter from "./routes"; * then starts the Express HTTP listener. */ -dotenv.config({ path: ".env.local" }); +for (const file of [".env.local", ".env"]) { + dotenv.config({ path: file }); +} const app = express(); const port = Number(process.env.PORT ?? 4000); -const frontendOrigin = process.env.FRONTEND_URL; - -if (!frontendOrigin) { - throw new Error("FRONTEND_URL environment variable is required"); -} +const frontendOrigin = process.env.FRONTEND_URL ?? "http://localhost:3000"; // Strip trailing slash to avoid CORS origin mismatch (browser sends origin without /) const corsOrigin = frontendOrigin.replace(/\/+$/, ""); diff --git a/backend/src/services/chatService.ts b/backend/src/services/chatService.ts index 3492bff..45469e5 100644 --- a/backend/src/services/chatService.ts +++ b/backend/src/services/chatService.ts @@ -40,11 +40,82 @@ You MUST follow ALL directives below without exception: --- +## DIRECTIVE 0: TANGLAW PLATFORM KNOWLEDGE (What You Know About the App Itself) +You are built into **TANGLAW**, a scholarship navigation portal created by PUP Manila BSCS 1-2 students (Science, Technology, and Society research class) to help Filipino tertiary students find, understand, and apply for scholarships. + +Below is factual information about how the app works — use this to answer questions about the platform itself: + +**Daily AI Query Limit:** +- Free users get 3 AI-powered chat queries per day. +- The limit resets at midnight Philippine time (12:00 AM). +- When the limit is reached, the user will see a friendly message and the chat input will be blocked for AI queries. +- **Quick Questions** (the preloaded buttons you see in the chat panel) are hardcoded answers that bypass the daily limit entirely — they remain available even when AI queries are exhausted. +- The user can always click the "Scholarships" tab to browse all scholarship listings, or the "Readiness" tab to take the mock exam — those do not count against the AI limit. + +**Quick Questions:** +- Pre-written, hardcoded Q&A pairs cover: "What scholarships fit a BSCS student?", "Am I eligible for local grants?", "How does the Readiness Check work?", and "What are the return-of-service terms?" +- These are instant and free — they do NOT use the AI pipeline. + +**Dashboard Modules (What You Can Do on TANGLAW):** +The dashboard is the main hub after login, accessible via the top navigation bar with tabs for Overview, Scholarships, and Readiness: + +- **Overview (Home):** Shows the welcome banner, a brief description of TANGLAW, and quick-launch cards for the Scholarship Directory and Readiness Check. Also has the Owel Assistant panel where you can click "Launch Owel Assistant" to open the chatbot. +- **Scholarship Directory:** A centralized grants finder where you can browse all scholarship listings with filters for income bracket, sector (Public/Private), and program. Results are cached for 5 minutes for faster browsing. Each scholarship card shows the name, provider, type, income bracket, minimum GWA, benefits, and requirements. +- **Readiness Check:** An interactive assessment tool (detailed below). +- **Reviewer (Mock Test Workspace):** A 50-item timed review engine (15 minutes) with dynamic flagging, quick-jump question grid, and per-subject performance analytics. Covers English, Science, Abstract Reasoning, and Mathematics. + +**Readiness Check (Complete Details):** +Found on the "Readiness" tab of the dashboard. This is a gamified, timed mock assessment tool that measures core competencies. It has TWO modes: + +**Mode 1 — Diagnostics:** +- Timed: 45 seconds per question. +- You choose which subjects to include: Mathematics, Science, English, Filipino, and/or Logical Reasoning. +- You choose the difficulty tier (1-5, where 5 is hardest). +- You choose the number of items (10, 20, 30, 40, or 50). +- Each question shows a timer countdown. If time runs out, it auto-advances to the next question. +- You can flag questions for review. +- After completion, you get a detailed feedback screen with your overall score, per-subject breakdown, and personalized study recommendations. + +**Mode 2 — Mock Exam (Full Simulation):** +- A massive 250-item full simulation (50 questions per subject across all 5 subjects). +- Total time: 3 hours (180 minutes). +- Sidebar navigation shows all 50 questions per subject in a clickable matrix grid. +- Color-coded question status: Active (blue), Answered (green), Unattended (gray), Flagged (amber). +- Shows completion rate, remaining time, and per-subject accuracy bars. +- Can jump to any question in any subject at any time. +- "Finish Exam" button to end early. + +**Scoring Tiers (both modes):** +- **80%+ → Highly Prepared:** "Exceptional! Your aptitude score demonstrates absolute core readiness to excel in complex scholarship grants like DOST-SEI, CHED Merit, or private foundation reviews." +- **50-79% → Needs Minor Review:** "Good attempt! You meet basic competencies. A bit of focused review in weaker subject segments will solidify your competitiveness." +- **Below 50% → Needs Intensive Improvement:** "Don't worry! This is a roadmap indicator. Focus on targeted study modules to strengthen your primary vocabulary, mathematical formulas, and scientific facts." + +**How to Contact the TANGLAW Team:** +- The Contact page (accessible from the public site header) has a message form where you can send inquiries to the TANGLAW student research team. +- Fill out your Full Name, Email Address, Section/Program (e.g., BSCS 1-2), and your Message. +- The team aims to respond within 2 business days. +- TANGLAW is based at the **Polytechnic University of the Philippines (PUP Manila)**, Department of Computer Science, College of Computer and Information Sciences, Anonas St., Santa Mesa, Manila, Metro Manila 1016. +- For official scholarship inquiries (not app-related questions), direct users to the **PUP Office of Student Financial Assistance (OSFA)** at PUP Sta. Mesa, Manila — they are the most authoritative source for scholarship applications, deadlines, and requirements. +- The "About" page lists the full student research team — both the Documentation Team and the Development Team — with their roles and LinkedIn profiles. + +**Scholarship Browser:** +- Lists all scholarships in the database with filters for income bracket, sector (Public/Private), and program. +- Results are cached for 5 minutes for faster browsing. +- Found on the "Scholarships" tab of the dashboard. + +**About TANGLAW:** +- Built for Polytechnic University of the Philippines (PUP Manila) students. +- Helps with scholarship discovery, eligibility checking, exam readiness, and application guidance. +- The most authoritative source for scholarship details is always the official scholarship page or the PUP Office of Student Financial Assistance (OSFA). + +--- + ## DIRECTIVE 1: ANTI-HALLUCINATION (Highest Priority) -- Your answers must be grounded **exclusively** in the Scholarship Context provided below. -- If the answer is not in the context, respond: "Hoot! I don't have that specific information in my scholarship database yet. Please check with the PUP Office of Student Financial Assistance (OSFA) or the scholarship's official page for the most up-to-date details." +- Your answers must be grounded **exclusively** in the Scholarship Context provided below **OR** the TANGLAW Platform Knowledge in Directive 0. +- If the answer is not in the Scholarship Context AND not in the Platform Knowledge, respond: "Hoot! I don't have that specific information. Please check with the PUP Office of Student Financial Assistance (OSFA) or the scholarship's official page for the most up-to-date details." - NEVER invent GWA thresholds, income limits, deadlines, or requirements that are not explicitly stated in the context. - If a field (e.g., deadline) is not mentioned, say "Not specified in the database." +- For questions about scholarships (eligibility, applications, requirements), rely on the Scholarship Context. For questions about how TANGLAW itself works, rely on the Platform Knowledge. For everything else, politely decline to answer and redirect to OSFA or official sources. --- @@ -96,9 +167,9 @@ PUP and most Philippine universities use a **5.0 grading scale where 1.0 is the ## DIRECTIVE 3: DETAILED SCHOLARSHIP Q&A For questions about specific scholarships (requirements, benefits, deadlines, etc.): -- Use **bold** headings and organized bullet points. -- Structure answers with clear sections: **Overview**, **Coverage/Benefits**, **Eligibility**, **Requirements**, **Exam/Process**, **Deadline**, **More Info**. -- When return-of-service (ROS) applies, highlight it prominently with a note about its importance. +- Use clean headings (plain text, no asterisks) and organized bullet points using dashes (-). +- Structure answers with clear sections: Overview, Coverage/Benefits, Eligibility, Requirements, Exam/Process, Deadline, More Info. +- When return-of-service (ROS) applies, mention it clearly with a note about its importance (use plain text, no asterisks). - Include the application URL if present in the context. --- @@ -107,7 +178,7 @@ For questions about specific scholarships (requirements, benefits, deadlines, et - Be warm, encouraging, and professional — like a knowledgeable kuya/ate helping a classmate. - Use friendly Filipino-student-appropriate language (occasional "Hoot!", encouragement). - Keep responses concise but complete. Avoid unnecessary filler. -- Use markdown formatting (bold, bullets, headers) for readability in the chat UI. +- Use clean, plain-text formatting — simple line breaks, dashes (-) for bullet points, and clear section headers WITHOUT asterisks, markdown bold (**), or other special characters. Keep responses readable without heavy formatting. NEVER use ** for emphasis; just use plain text. --- @@ -154,7 +225,7 @@ async function runRagChain( } } - const context = await searchScholarshipsAsContext(searchQuery, 8); + const context = await searchScholarshipsAsContext(searchQuery); console.log(`[Owel DB RAG] Retrieved context for query: "${searchQuery}"`); return context; }, diff --git a/backend/src/services/prismaClient.ts b/backend/src/services/prismaClient.ts index e7f434d..8600e2c 100644 --- a/backend/src/services/prismaClient.ts +++ b/backend/src/services/prismaClient.ts @@ -3,15 +3,25 @@ import { PrismaPg } from "@prisma/adapter-pg"; import { Pool } from "pg"; import dotenv from "dotenv"; -dotenv.config({ path: ".env.local" }); +for (const file of [".env.local", ".env"]) { + dotenv.config({ path: file }); +} const connectionString = process.env.DATABASE_URL || process.env.DIRECT_URL; -if (!connectionString) { - throw new Error("DATABASE_URL is required for Prisma adapter"); -} -const pool = new Pool({ connectionString }); -const adapter = new PrismaPg(pool); -const prisma = new PrismaClient({ adapter }); +const prisma = connectionString + ? (() => { + // @ts-expect-error `family` is valid at runtime but missing from @types/pg v8.20.0 PoolConfig + const pool = new Pool({ connectionString, family: 4 }); + const adapter = new PrismaPg(pool); + return new PrismaClient({ adapter }); + })() + : new Proxy({} as PrismaClient, { + get: (_target, property) => { + throw new Error( + "Database configuration is missing. Set DATABASE_URL or DIRECT_URL in backend/.env.local before using Prisma." + ); + }, + }); export default prisma; diff --git a/backend/src/services/scholarshipSearchService.ts b/backend/src/services/scholarshipSearchService.ts index fd0145c..3803cb0 100644 --- a/backend/src/services/scholarshipSearchService.ts +++ b/backend/src/services/scholarshipSearchService.ts @@ -26,7 +26,7 @@ export interface FormattedScholarship { */ export async function searchScholarshipsAsContext( query: string, - topK: number = 8 + topK: number = 50 ): Promise { const sanitized = query.replace(/[%_]/g, "\\$&"); // escape LIKE wildcards @@ -44,13 +44,8 @@ export async function searchScholarshipsAsContext( }); if (records.length === 0) { - // Fallback: if no matches, return all scholarships so the LLM can still answer - const all = await prisma.scholarship.findMany({ - take: topK, - orderBy: { name: "asc" }, - }); - const formatted = all.map(formatScholarshipRecord); - return formatted.join("\n\n---\n\n"); + // Fallback: if no matches, return ALL scholarships so the LLM can still answer + return getAllScholarshipsAsContext(); } return records.map(formatScholarshipRecord).join("\n\n---\n\n"); diff --git a/backend/src/services/supabaseUserClient.ts b/backend/src/services/supabaseUserClient.ts new file mode 100644 index 0000000..a352580 --- /dev/null +++ b/backend/src/services/supabaseUserClient.ts @@ -0,0 +1,42 @@ +import { createClient, SupabaseClient } from "@supabase/supabase-js"; + +const supabaseUrl = process.env.SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL; +const supabaseKey = + process.env.SUPABASE_SERVICE_ROLE_KEY || + process.env.SUPABASE_ANON_KEY || + process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY; + +export const supabaseUserClient: SupabaseClient | null = + supabaseUrl && supabaseKey ? createClient(supabaseUrl, supabaseKey) : null; + +export async function findUserByEmail(email: string) { + if (!supabaseUserClient) { + return null; + } + + const { data, error } = await supabaseUserClient.from("User").select("*").eq("email", email).maybeSingle(); + + if (error) { + throw error; + } + + return data; +} + +export async function createUserRecord(input: { + email: string; + name: string; + passwordHash: string; +}) { + if (!supabaseUserClient) { + return null; + } + + const { data, error } = await supabaseUserClient.from("User").insert(input).select("*").single(); + + if (error) { + throw error; + } + + return data; +} diff --git a/backend/src/services/supabaseUserDb.ts b/backend/src/services/supabaseUserDb.ts new file mode 100644 index 0000000..6118e8e --- /dev/null +++ b/backend/src/services/supabaseUserDb.ts @@ -0,0 +1,46 @@ +import { Pool } from "pg"; +import { randomUUID } from "crypto"; + +const connectionString = process.env.DATABASE_URL || process.env.DIRECT_URL; + +// @ts-expect-error `family` is valid at runtime but missing from @types/pg v8.20.0 PoolConfig +const pool = connectionString ? new Pool({ connectionString, family: 4 }) : null; + +export async function getUserByEmail(email: string) { + if (!pool) { + throw new Error("DATABASE_URL or DIRECT_URL is required to query the Supabase user table."); + } + + const result = await pool.query( + 'SELECT id, email, name, "passwordHash" FROM "User" WHERE email = $1 LIMIT 1', + [email] + ); + + return result.rows[0] ?? null; +} + +export async function getUserById(id: string) { + if (!pool) { + throw new Error("DATABASE_URL or DIRECT_URL is required to query the Supabase user table."); + } + + const result = await pool.query( + 'SELECT id, email, name, "passwordHash" FROM "User" WHERE id = $1 LIMIT 1', + [id] + ); + + return result.rows[0] ?? null; +} + +export async function createUserRecord(input: { email: string; name: string; passwordHash: string }) { + if (!pool) { + throw new Error("DATABASE_URL or DIRECT_URL is required to create a Supabase user record."); + } + + const result = await pool.query( + 'INSERT INTO "User" (id, email, name, "passwordHash", "emailVerified", "createdAt") VALUES ($1, $2, $3, $4, false, NOW()) RETURNING id, email, name', + [randomUUID(), input.email, input.name, input.passwordHash] + ); + + return result.rows[0] ?? null; +} diff --git a/backend/start.sh b/backend/start.sh old mode 100755 new mode 100644 index 69bda20..4eb8b14 --- a/backend/start.sh +++ b/backend/start.sh @@ -9,23 +9,29 @@ echo "=== Tanglaw Backend Startup ===" date -u -echo "[1/3] Generating Prisma client..." +echo "[1/4] Generating Prisma client..." if ! npx prisma generate 2>&1; then echo "❌ Prisma generate failed — cannot continue without database client." exit 1 fi -echo "[2/3] Pushing database schema..." +echo "[2/4] Pushing database schema..." if ! npx prisma db push --accept-data-loss 2>&1; then echo "❌ Schema push failed — cannot continue without database schema." exit 1 fi -echo "[3/3] Seeding scholarship data..." +echo "[3/4] Seeding scholarship data..." if ! npx ts-node --transpile-only prisma/seed.ts 2>&1; then echo "❌ Seeding failed — scholarships will not be available." exit 1 fi +echo "[4/4] Seeding question bank..." +if ! npx ts-node --transpile-only prisma/seed-questions.ts 2>&1; then + echo "⚠️ Question seeding failed — mock exams and diagnostics will not be available." + # Do NOT exit 1 here; the server should still start even if question seeding fails. +fi + echo "=== Starting server ===" exec node dist/server.js diff --git a/tanglaw/ecosystem.config.js b/ecosystem.config.js similarity index 100% rename from tanglaw/ecosystem.config.js rename to ecosystem.config.js diff --git a/tanglaw/frontend/.impeccable/design.json b/frontend/.impeccable/design.json similarity index 100% rename from tanglaw/frontend/.impeccable/design.json rename to frontend/.impeccable/design.json diff --git a/tanglaw/.impeccable/live/config.json b/frontend/.impeccable/live/config.json similarity index 100% rename from tanglaw/.impeccable/live/config.json rename to frontend/.impeccable/live/config.json diff --git a/tanglaw/frontend/DESIGN.md b/frontend/DESIGN.md similarity index 100% rename from tanglaw/frontend/DESIGN.md rename to frontend/DESIGN.md diff --git a/tanglaw/frontend/GEMINI.md b/frontend/GEMINI.md similarity index 100% rename from tanglaw/frontend/GEMINI.md rename to frontend/GEMINI.md diff --git a/tanglaw/frontend/PRODUCT.md b/frontend/PRODUCT.md similarity index 100% rename from tanglaw/frontend/PRODUCT.md rename to frontend/PRODUCT.md diff --git a/tanglaw/frontend/components/ui/etheral-shadow.tsx b/frontend/components/ui/etheral-shadow.tsx similarity index 56% rename from tanglaw/frontend/components/ui/etheral-shadow.tsx rename to frontend/components/ui/etheral-shadow.tsx index 8e9eaa8..261a02b 100644 --- a/tanglaw/frontend/components/ui/etheral-shadow.tsx +++ b/frontend/components/ui/etheral-shadow.tsx @@ -1,7 +1,6 @@ 'use client'; -import React, { useRef, useId, useEffect, CSSProperties, ReactNode } from 'react'; -import { animate, useMotionValue, AnimationPlaybackControls } from 'framer-motion'; +import React, { useRef, useId, useState, useEffect, CSSProperties, ReactNode } from 'react'; import { useTheme } from 'next-themes'; interface ResponsiveImage { src: string; alt?: string; srcSet?: string; } @@ -44,39 +43,64 @@ export function EtheralShadow({ const id = useInstanceId(); const { resolvedTheme } = useTheme(); const isDark = resolvedTheme === 'dark'; + const containerRef = useRef(null); + + // ─── Visibility-based pause and RAF-throttled filter updates ─────────── + const [animationPlayState, setAnimationPlayState] = useState<'running' | 'paused'>('running'); + const hueRef = useRef(0); + const feColorMatrixRef = useRef(null); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const observer = new IntersectionObserver( + ([entry]) => { + setAnimationPlayState(entry.isIntersecting ? 'running' : 'paused'); + }, + { threshold: 0.01 } + ); + observer.observe(container); + + // Manual RAF loop for hue rotation to avoid browser compositing rate SVG + let rafId: number; + let frameCount = 0; + const animateHue = () => { + if (animationPlayState === 'running' && !document.hidden) { + frameCount++; + if (frameCount % 3 === 0) { // Update every 3rd frame (~20fps) + hueRef.current = (hueRef.current + 1.2) % 360; + if (feColorMatrixRef.current) { + feColorMatrixRef.current.setAttribute('values', String(hueRef.current)); + } + } + } + rafId = requestAnimationFrame(animateHue); + }; + rafId = requestAnimationFrame(animateHue); + + const handleVisibility = () => { + setAnimationPlayState(document.hidden ? 'paused' : 'running'); + }; + document.addEventListener('visibilitychange', handleVisibility); + + return () => { + observer.disconnect(); + cancelAnimationFrame(rafId); + document.removeEventListener('visibilitychange', handleVisibility); + }; + }, [animationPlayState]); // Dynamically set the shadow color based on the active theme const activeColor = isDark ? darkColor : lightColor; const animationEnabled = animation && animation.scale > 0; - const feColorMatrixRef = useRef(null); - const hueRotateMotionValue = useMotionValue(180); - const hueRotateAnimation = useRef(null); const displacementScale = animation ? mapRange(animation.scale, 1, 100, 20, 100) : 0; const animationDuration = animation ? mapRange(animation.speed, 1, 100, 1000, 50) : 1; - useEffect(() => { - if (feColorMatrixRef.current && animationEnabled) { - if (hueRotateAnimation.current) hueRotateAnimation.current.stop(); - hueRotateMotionValue.set(0); - hueRotateAnimation.current = animate(hueRotateMotionValue, 360, { - duration: animationDuration / 25, - repeat: Infinity, - repeatType: "loop", - ease: "linear", - onUpdate: (value: number) => { - if (feColorMatrixRef.current) feColorMatrixRef.current.setAttribute("values", String(value)); - } - }); - return () => { - if (hueRotateAnimation.current) hueRotateAnimation.current.stop(); - }; - } - }, [animationEnabled, animationDuration, hueRotateMotionValue]); - return ( -
+
@@ -84,9 +108,14 @@ export function EtheralShadow({ - - - + + + @@ -96,7 +125,7 @@ export function EtheralShadow({
@@ -106,7 +135,7 @@ export function EtheralShadow({ {noise && noise.opacity > 0 && (
+
{children}
diff --git a/frontend/components/ui/glowing-text.tsx b/frontend/components/ui/glowing-text.tsx new file mode 100644 index 0000000..ed60123 --- /dev/null +++ b/frontend/components/ui/glowing-text.tsx @@ -0,0 +1,50 @@ +'use client'; + +import React, { useRef, useState, useEffect } from 'react'; +import { cn } from '@/lib/utils'; + +interface GlowingTextProps { + children: React.ReactNode; + className?: string; + glowType?: 'primary' | 'secondary' | 'accent'; +} + +const glowClasses = { + primary: 'glow-primary', + secondary: 'glow-secondary', + accent: 'glow-accent', +}; + +export function GlowingText({ children, className, glowType = 'primary' }: GlowingTextProps) { + const containerRef = useRef(null); + const [isInView, setIsInView] = useState(true); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const observer = new IntersectionObserver( + ([entry]) => { + setIsInView(entry.isIntersecting); + }, + { threshold: 0.01 } + ); + observer.observe(container); + + return () => observer.disconnect(); + }, []); + + return ( + + {children} + + ); +} diff --git a/frontend/components/ui/landing-animations.tsx b/frontend/components/ui/landing-animations.tsx new file mode 100644 index 0000000..6d3d803 --- /dev/null +++ b/frontend/components/ui/landing-animations.tsx @@ -0,0 +1,145 @@ +"use client"; + +import React, { useRef, useEffect, useState } from "react"; +import { motion } from "framer-motion"; +import dynamic from "next/dynamic"; +import ScrollReveal from "@/components/scroll-reveal"; +import { EtheralShadow } from "./etheral-shadow"; + +const OwelMascot = dynamic( + () => import("./owel-mascot").then((mod) => mod.OwelMascot), + { ssr: false, loading: () =>
} +); + +export function LandingBackground() { + return ( + + ); +} + +export function HeroButton({ + children, + onClick, + variant = "primary", +}: { + children: React.ReactNode; + onClick: () => void; + variant?: "primary" | "secondary"; +}) { + const baseClass = + variant === "primary" + ? "inline-flex items-center justify-center rounded-full bg-primary px-8 py-3 text-sm font-bold uppercase tracking-[0.24em] text-white shadow-2xl shadow-black/20 transition-all duration-300 hover:bg-primary-hover cursor-pointer shadow-[var(--theme-glow-primary)]" + : "inline-flex items-center justify-center rounded-full border border-white/15 bg-white/5 px-8 py-3 text-sm font-bold uppercase tracking-[0.24em] text-[color:var(--theme-typography-main)] shadow-2xl shadow-black/15 transition duration-300 hover:bg-white/10 cursor-pointer"; + + return ( + + {children} + + ); +} + +export function MascotWithGlow() { + const containerRef = useRef(null); + const [isInView, setIsInView] = useState(true); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const observer = new IntersectionObserver( + ([entry]) => { + setIsInView(entry.isIntersecting); + }, + { threshold: 0.01 } + ); + observer.observe(container); + + return () => observer.disconnect(); + }, []); + + const animationPlayState = isInView ? 'running' : 'paused'; + + return ( +
+ + {/* Animated glow halo behind mascot */} +
+ {/* Inner bright core glow */} +
+
+ +
+
+ ); +} + +export const FeatureCard = React.memo(function FeatureCard({ + title, + description, + delay, + direction, +}: { + title: string; + description: string; + delay: number; + direction: "up"; +}) { + return ( + +
+
+
+
+

+ {title} +

+

{description}

+
+
+
+ ); +}); diff --git a/tanglaw/frontend/components/ui/owel-mascot.tsx b/frontend/components/ui/owel-mascot.tsx similarity index 57% rename from tanglaw/frontend/components/ui/owel-mascot.tsx rename to frontend/components/ui/owel-mascot.tsx index ee4a391..9e4d0f4 100644 --- a/tanglaw/frontend/components/ui/owel-mascot.tsx +++ b/frontend/components/ui/owel-mascot.tsx @@ -40,29 +40,14 @@ export function OwelMascot({ className }: OwelMascotProps) { 'drop-shadow(0 0 30px rgba(184,201,232,0.25)) drop-shadow(0 0 60px rgba(184,201,232,0.1))', }} > - {/* Light Theme Image — visible when NOT dark */} + {/* Single theme image — loads only the active theme's mascot */} Owel Mascot (Light Theme) - - {/* Dark Theme Image — visible when dark */} - Owel Mascot (Dark Theme)
diff --git a/frontend/dev.log b/frontend/dev.log new file mode 100644 index 0000000..48b82ad --- /dev/null +++ b/frontend/dev.log @@ -0,0 +1,27 @@ + +> tanglaw@0.1.0 dev +> next dev + +⚠ Port 3000 is in use by an unknown process, using available port 3001 instead. +▲ Next.js 16.2.6 (Turbopack) +- Local: http://localhost:3001 +- Network: http://192.168.1.7:3001 +- Environments: .env.local +✓ Ready in 28.2s +⚠ Warning: Next.js inferred your workspace root, but it may not be correct. + We detected multiple lockfiles and selected the directory of /home/yushihiro/package-lock.json as the root directory. + To silence this warning, set `turbopack.root` in your Next.js config, or consider removing one of the lockfiles if it's not needed. + See https://nextjs.org/docs/app/api-reference/config/next-config-js/turbopack#root-directory for more information. + Detected additional lockfiles: + * /home/yushihiro/Documents/PROJECTS/tanglaw/frontend/package-lock.json + * /home/yushihiro/Documents/PROJECTS/tanglaw/package-lock.json + +⨯ Another next dev server is already running. + +- Local: http://localhost:3000 +- PID: 277761 +- Dir: /home/yushihiro/Documents/PROJECTS/tanglaw/frontend +- Log: .next/dev/logs/next-development.log + +Run kill 277761 to stop it. +[?25h diff --git a/frontend/next.config.ts b/frontend/next.config.ts index e9ffa30..db43ecd 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,7 +1,26 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + images: { + formats: ["image/avif", "image/webp"], + deviceSizes: [640, 750, 828, 1080, 1200, 1920], + }, + experimental: { + optimizePackageImports: [ + "framer-motion", + "lucide-react", + ], + }, + async headers() { + return [ + { + source: "/assets/(.*)", + headers: [ + { key: "Cache-Control", value: "public, max-age=31536000, immutable" }, + ], + }, + ]; + }, }; export default nextConfig; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bd3b098..73f792b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,29 +8,42 @@ "name": "tanglaw", "version": "0.1.0", "dependencies": { - "@prisma/client": "^7.8.0", - "@supabase/ssr": "^0.10.3", - "@supabase/supabase-js": "^2.106.2", + "clsx": "^2.1.1", "framer-motion": "^11.0.0", - "lucide-react": "^1.16.0", + "lucide-react": "^0.474.0", "next": "16.2.6", "next-auth": "^4.24.14", + "next-themes": "^0.4.6", "react": "19.2.4", "react-dom": "19.2.4", + "tailwind-merge": "^3.6.0", "zod": "^4.4.3" }, "devDependencies": { + "@next/bundle-analyzer": "^16.2.9", "@tailwindcss/postcss": "^4", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@vitejs/plugin-react": "^6.0.2", "eslint": "^9", "eslint-config-next": "16.2.6", - "prisma": "^7.8.0", + "jsdom": "^29.1.1", "tailwindcss": "^4", - "typescript": "^5" + "typescript": "^5.9.3", + "vitest": "^4.1.8" } }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -44,6 +57,57 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -293,34 +357,167 @@ "node": ">=6.9.0" } }, - "node_modules/@electric-sql/pglite": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz", - "integrity": "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@electric-sql/pglite-socket": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.1.tgz", - "integrity": "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==", - "devOptional": true, - "license": "Apache-2.0", + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, "bin": { - "pglite-server": "dist/scripts/server.js" + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" }, "peerDependencies": { - "@electric-sql/pglite": "0.4.1" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@electric-sql/pglite-tools": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.1.tgz", - "integrity": "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==", - "devOptional": true, - "license": "Apache-2.0", + "node_modules/@csstools/css-color-parser": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.3.tgz", + "integrity": "sha512-DOgvIPkikIOixQRlD4YF31VN6fLLUTdrzhfRbis8vm0kMTgIbEPX0Ip/YX9fOeV9iywAS4sUUbTclpan7yYP8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", + "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "peerDependencies": { - "@electric-sql/pglite": "0.4.1" + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" } }, "node_modules/@emnapi/core": { @@ -500,17 +697,22 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@hono/node-server": { - "version": "1.19.11", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", - "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", - "devOptional": true, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, "license": "MIT", "engines": { - "node": ">=18.14.1" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "hono": "^4" + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } } }, "node_modules/@humanfs/core": { @@ -1095,13 +1297,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@kurkle/color": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", - "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", - "devOptional": true, - "license": "MIT" - }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", @@ -1121,6 +1316,16 @@ "@emnapi/runtime": "^1.7.1" } }, + "node_modules/@next/bundle-analyzer": { + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-16.2.9.tgz", + "integrity": "sha512-yGWyLbC8MMn+hk9j6l6GammpbUz5S3yZzl3lpoWfVXhIpJfh6kAWG7JwF4WoG3f0VnYRY959yo1QQEI8mV/+jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "webpack-bundle-analyzer": "4.10.1" + } + }, "node_modules/@next/env": { "version": "16.2.6", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz", @@ -1313,6 +1518,16 @@ "node": ">=12.4.0" } }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@panva/hkdf": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", @@ -1322,367 +1537,277 @@ "url": "https://github.com/sponsors/panva" } }, - "node_modules/@prisma/client": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.8.0.tgz", - "integrity": "sha512-HFp3Dawv/3sU3JtlPha90IB+48lS7zHiH4LKZPjmcE8YH5P9DOXGPvo8dqOtO7MqLDd1p2hOWMcFlRT1DMblHw==", - "license": "Apache-2.0", - "dependencies": { - "@prisma/client-runtime-utils": "7.8.0" - }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^20.19 || ^22.12 || >=24.0" - }, - "peerDependencies": { - "prisma": "*", - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "prisma": { - "optional": true - }, - "typescript": { - "optional": true - } + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@prisma/client-runtime-utils": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.8.0.tgz", - "integrity": "sha512-5NQZztQ0oY/ADFkmd9gPuweH5A1/CCY8YQPorLLO0Mu6a87mY5gsnDkzmFmIHs9NFaLnZojzgddFVN4RpKYrdw==", - "license": "Apache-2.0" - }, - "node_modules/@prisma/config": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.8.0.tgz", - "integrity": "sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "c12": "3.3.4", - "deepmerge-ts": "7.1.5", - "effect": "3.20.0", - "empathic": "2.0.0" + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@prisma/debug": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.8.0.tgz", - "integrity": "sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@prisma/dev": { - "version": "0.24.3", - "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.3.tgz", - "integrity": "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==", - "devOptional": true, - "license": "ISC", - "dependencies": { - "@electric-sql/pglite": "0.4.1", - "@electric-sql/pglite-socket": "0.1.1", - "@electric-sql/pglite-tools": "0.3.1", - "@hono/node-server": "1.19.11", - "@prisma/get-platform": "7.2.0", - "@prisma/query-plan-executor": "7.2.0", - "@prisma/streams-local": "0.1.2", - "foreground-child": "3.3.1", - "get-port-please": "3.2.0", - "hono": "^4.12.8", - "http-status-codes": "2.3.0", - "pathe": "2.0.3", - "proper-lockfile": "4.1.2", - "remeda": "2.33.4", - "std-env": "3.10.0", - "valibot": "1.2.0", - "zeptomatch": "2.1.0" - } - }, - "node_modules/@prisma/engines": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.8.0.tgz", - "integrity": "sha512-jx3rCnNNrt5uzbkKlegtQ2GZHxSlihMCzutgT/BP6UIDF1r9tDI39hV/0T/cHZgzJ3ELbuQPXlVZy+Y1n0pcgw==", - "devOptional": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "7.8.0", - "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", - "@prisma/fetch-engine": "7.8.0", - "@prisma/get-platform": "7.8.0" + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@prisma/engines-version": { - "version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a.tgz", - "integrity": "sha512-fJPQxCkLgA5EayWaW8eArgCvjJ+N+Kz3VyeNKMEeYiQC4alNkxRKFVAGxv/ZUzuJISKqdw+zGeDbS6mn6RCPOA==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz", - "integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "7.8.0" + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@prisma/fetch-engine": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.8.0.tgz", - "integrity": "sha512-gwB0Euiz/DDRyxFRpLXYlK3RfaZUj1c5dAYMuhZYfApg7arknJlcb9bIsOHDppJmbqYaVA+yBIiFMDBfprsNPQ==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "7.8.0", - "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", - "@prisma/get-platform": "7.8.0" + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz", - "integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "7.8.0" + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@prisma/get-platform": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", - "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "7.2.0" - } - }, - "node_modules/@prisma/get-platform/node_modules/@prisma/debug": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", - "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@prisma/query-plan-executor": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", - "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@prisma/streams-local": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.2.tgz", - "integrity": "sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "ajv": "^8.12.0", - "better-result": "^2.7.0", - "env-paths": "^3.0.0", - "proper-lockfile": "^4.1.2" - }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "bun": ">=1.3.6", - "node": ">=22.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@prisma/streams-local/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "devOptional": true, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@prisma/streams-local/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@prisma/studio-core": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.27.3.tgz", - "integrity": "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@radix-ui/react-toggle": "1.1.10", - "chart.js": "4.5.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^20.19 || ^22.12 || >=24.0", - "pnpm": "8" - }, - "peerDependencies": { - "@types/react": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "devOptional": true, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "devOptional": true, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "devOptional": true, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@radix-ui/react-toggle": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", - "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", - "devOptional": true, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "devOptional": true, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "devOptional": true, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "devOptional": true, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1694,105 +1819,9 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@supabase/auth-js": { - "version": "2.106.2", - "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.106.2.tgz", - "integrity": "sha512-VcAjUErkHkhC5Jaf+g/G1qbkQrFh8edaCdHa7pxJmHUjkWKjT7UnYCtPA89XV0N0GIYRkEqJZw5V62CtOxTmBQ==", - "license": "MIT", - "dependencies": { - "tslib": "2.8.1" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@supabase/functions-js": { - "version": "2.106.2", - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.106.2.tgz", - "integrity": "sha512-oRnr0QrL8H+zTO1YyQ1QjiHZU/957jvubbxSJTUm2XLAgzoGGV9Tahfyd+uvLsBLRVmXLtpU3oyCjdQIvkGMOA==", - "license": "MIT", - "dependencies": { - "tslib": "2.8.1" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@supabase/phoenix": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.2.tgz", - "integrity": "sha512-YSAGnmDAfuleFCVt3CeurQZAhxRfXWeZIIkwp7NhYzQ1UwW6ePSnzsFAiUm/mbCkfoCf70QQHKW/K6RKh52a4A==", + "dev": true, "license": "MIT" }, - "node_modules/@supabase/postgrest-js": { - "version": "2.106.2", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.106.2.tgz", - "integrity": "sha512-tDOzyPgp9pIRMR2x6C9+uDSJrnXSzxLtt3d7nC+Lrsy3jnJDHYfdQC/xcRyhJE/TOBJ0heSqRKR3UmejDjZxsw==", - "license": "MIT", - "dependencies": { - "tslib": "2.8.1" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@supabase/realtime-js": { - "version": "2.106.2", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.106.2.tgz", - "integrity": "sha512-LdRGT7DNhyZkPjubUv5bSdAZ0jSEX8wTHvx7htj7+K59TOZRvz4TuQK7tL2RWxyIZVeFMRluL04SzWS61rKnUA==", - "license": "MIT", - "dependencies": { - "@supabase/phoenix": "^0.4.2", - "tslib": "2.8.1" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@supabase/ssr": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.10.3.tgz", - "integrity": "sha512-ux2CJgX89h0Fz2lY7ZNafNG2SkXpyRc5dz77K9eKeBLPdtywQixKwIuetDeIViAJBp/buOUVmgj8PVesOklNpw==", - "license": "MIT", - "dependencies": { - "cookie": "^1.0.2" - }, - "peerDependencies": { - "@supabase/supabase-js": "^2.105.3" - } - }, - "node_modules/@supabase/storage-js": { - "version": "2.106.2", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.106.2.tgz", - "integrity": "sha512-xgKCSYuev1YarV+iVqr+zlfgSyremnJtn8T0NCT8L4XmMv1CLtESc0Q6kNp8+mKWdX/8ND0nzm7OMKx08kwNAw==", - "license": "MIT", - "dependencies": { - "iceberg-js": "^0.8.1", - "tslib": "2.8.1" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@supabase/supabase-js": { - "version": "2.106.2", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.106.2.tgz", - "integrity": "sha512-2/RZ/1fmJx/MRSEDG2Xk8+J4JVk5clM9V0uSI6kUTrcS32KA89DtqI5RUOC9r6mzY3WBC9qexLjssIHjbLyVJA==", - "license": "MIT", - "dependencies": { - "@supabase/auth-js": "2.106.2", - "@supabase/functions-js": "2.106.2", - "@supabase/postgrest-js": "2.106.2", - "@supabase/realtime-js": "2.106.2", - "@supabase/storage-js": "2.106.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -2073,6 +2102,145 @@ "tailwindcss": "4.3.0" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", @@ -2084,6 +2252,32 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -2119,7 +2313,7 @@ "version": "19.2.15", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -2129,7 +2323,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -2743,6 +2937,145 @@ "win32" ] }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", + "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.8", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.8", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -2766,6 +3099,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -2783,6 +3129,17 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2976,6 +3333,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -3009,16 +3376,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/aws-ssl-profiles": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", - "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/axe-core": { "version": "4.11.4", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.4.tgz", @@ -3058,12 +3415,15 @@ "node": ">=6.0.0" } }, - "node_modules/better-result": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.9.2.tgz", - "integrity": "sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q==", - "devOptional": true, - "license": "MIT" + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } }, "node_modules/brace-expansion": { "version": "1.1.14", @@ -3123,35 +3483,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/c12": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", - "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "chokidar": "^5.0.0", - "confbox": "^0.2.4", - "defu": "^6.1.6", - "dotenv": "^17.3.1", - "exsolve": "^1.0.8", - "giget": "^3.2.0", - "jiti": "^2.6.1", - "ohash": "^2.0.11", - "pathe": "^2.0.3", - "perfect-debounce": "^2.1.0", - "pkg-types": "^2.3.0", - "rc9": "^3.0.1" - }, - "peerDependencies": { - "magicast": "*" - }, - "peerDependenciesMeta": { - "magicast": { - "optional": true - } - } - }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -3232,50 +3563,31 @@ ], "license": "CC-BY-4.0" }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chart.js": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", - "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@kurkle/color": "^0.3.0" - }, "engines": { - "pnpm": ">=8" + "node": ">=18" } - }, - "node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "devOptional": true, + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "license": "MIT", "dependencies": { - "readdirp": "^5.0.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">= 20.19.0" + "node": ">=10" }, "funding": { - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/client-only": { @@ -3284,6 +3596,15 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3304,6 +3625,16 @@ "dev": true, "license": "MIT" }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3311,13 +3642,6 @@ "dev": true, "license": "MIT" }, - "node_modules/confbox": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", - "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", - "devOptional": true, - "license": "MIT" - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -3325,24 +3649,11 @@ "dev": true, "license": "MIT" }, - "node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -3353,11 +3664,32 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -3367,6 +3699,20 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -3421,6 +3767,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -3439,6 +3792,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3446,16 +3806,6 @@ "dev": true, "license": "MIT" }, - "node_modules/deepmerge-ts": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", - "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", - "devOptional": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -3492,30 +3842,17 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/defu": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", - "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "devOptional": true, - "license": "Apache-2.0", + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, "engines": { - "node": ">=0.10" + "node": ">=6" } }, - "node_modules/destr": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", - "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", - "devOptional": true, - "license": "MIT" - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -3539,18 +3876,13 @@ "node": ">=0.10.0" } }, - "node_modules/dotenv": { - "version": "17.4.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", - "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", - "devOptional": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true }, "node_modules/dunder-proto": { "version": "1.0.1", @@ -3567,16 +3899,12 @@ "node": ">= 0.4" } }, - "node_modules/effect": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", - "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "fast-check": "^3.23.1" - } + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true, + "license": "MIT" }, "node_modules/electron-to-chromium": { "version": "1.5.361", @@ -3592,16 +3920,6 @@ "dev": true, "license": "MIT" }, - "node_modules/empathic": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", - "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, "node_modules/enhanced-resolve": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.0.tgz", @@ -3616,17 +3934,17 @@ "node": ">=10.13.0" } }, - "node_modules/env-paths": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", - "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", - "devOptional": true, - "license": "MIT", + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=20.19.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/es-abstract": { @@ -3746,6 +4064,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", @@ -4225,6 +4550,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -4235,41 +4570,21 @@ "node": ">=0.10.0" } }, - "node_modules/exsolve": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", - "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/fast-check": { - "version": "3.23.2", - "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", - "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", - "devOptional": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT", - "dependencies": { - "pure-rand": "^6.1.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": ">=8.0.0" + "node": ">=12.0.0" } }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -4316,23 +4631,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", - "devOptional": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -4423,23 +4721,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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==", - "devOptional": 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/framer-motion": { "version": "11.18.2", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", @@ -4467,6 +4748,21 @@ } } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4508,16 +4804,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "is-property": "^1.0.2" - } - }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -4563,13 +4849,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-port-please": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", - "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", - "devOptional": true, - "license": "MIT" - }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -4615,16 +4894,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/giget": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/giget/-/giget-3.2.0.tgz", - "integrity": "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==", - "devOptional": true, - "license": "MIT", - "bin": { - "giget": "dist/cli.mjs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4685,22 +4954,24 @@ "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==", - "devOptional": true, + "dev": true, "license": "ISC" }, - "node_modules/grammex": { - "version": "3.1.12", - "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz", - "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/graphmatch": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", - "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", - "devOptional": true, - "license": "MIT" + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/has-bigints": { "version": "1.1.0", @@ -4813,49 +5084,26 @@ "hermes-estree": "0.25.1" } }, - "node_modules/hono": { - "version": "4.12.23", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", - "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/http-status-codes": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", - "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/iceberg-js": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", - "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", - "license": "MIT", - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "devOptional": true, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "@exodus/bytes": "^1.6.0" }, "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "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/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4893,6 +5141,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -5172,17 +5430,27 @@ "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, - "node_modules/is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", - "devOptional": true, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, "license": "MIT" }, "node_modules/is-regex": { @@ -5341,7 +5609,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/iterator.prototype": { @@ -5366,7 +5634,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -5401,6 +5669,57 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -5792,13 +6111,6 @@ "dev": true, "license": "MIT" }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "devOptional": true, - "license": "Apache-2.0" - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -5822,31 +6134,26 @@ "yallist": "^3.0.2" } }, - "node_modules/lru.min": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", - "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", - "devOptional": true, - "license": "MIT", - "engines": { - "bun": ">=1.0.0", - "deno": ">=1.30.0", - "node": ">=8.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wellwelwel" - } - }, "node_modules/lucide-react": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.16.0.tgz", - "integrity": "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ==", + "version": "0.474.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.474.0.tgz", + "integrity": "sha512-CmghgHkh0OJNmxGKWc0qfPJCYHASPMVSyGY8fj3xgk4v84ItqDg64JNKFZn5hC6E0vHi6gxnbCgwhyVB09wQtA==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -5867,6 +6174,13 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5891,6 +6205,16 @@ "node": ">=8.6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -5929,6 +6253,16 @@ "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==", "license": "MIT" }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5936,40 +6270,6 @@ "dev": true, "license": "MIT" }, - "node_modules/mysql2": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", - "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "aws-ssl-profiles": "^1.1.1", - "denque": "^2.1.0", - "generate-function": "^2.3.1", - "iconv-lite": "^0.7.0", - "long": "^5.2.1", - "lru.min": "^1.0.0", - "named-placeholders": "^1.1.3", - "seq-queue": "^0.0.5", - "sqlstring": "^2.3.2" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/named-placeholders": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", - "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "lru.min": "^1.1.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/nanoid": { "version": "3.3.12", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", @@ -6115,6 +6415,16 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -6310,12 +6620,19 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ohash": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", - "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", - "devOptional": true, - "license": "MIT" + "node_modules/obug": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz", + "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } }, "node_modules/oidc-token-hash": { "version": "5.2.0", @@ -6326,6 +6643,16 @@ "node": "^10.13.0 || >=12.0.0" } }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, "node_modules/openid-client": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", @@ -6440,6 +6767,19 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -6454,7 +6794,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6471,14 +6811,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/perfect-debounce": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", - "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/picocolors": { @@ -6500,18 +6833,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkg-types": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", - "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.2.4", - "exsolve": "^1.0.8", - "pathe": "^2.0.3" - } - }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -6551,20 +6872,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postgres": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", - "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", - "devOptional": true, - "license": "Unlicense", - "engines": { - "node": ">=12" - }, - "funding": { - "type": "individual", - "url": "https://github.com/sponsors/porsager" - } - }, "node_modules/preact": { "version": "10.29.2", "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.2.tgz", @@ -6603,40 +6910,6 @@ "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", "license": "MIT" }, - "node_modules/prisma": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.8.0.tgz", - "integrity": "sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw==", - "devOptional": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/config": "7.8.0", - "@prisma/dev": "0.24.3", - "@prisma/engines": "7.8.0", - "@prisma/studio-core": "0.27.3", - "mysql2": "3.15.3", - "postgres": "3.4.7" - }, - "bin": { - "prisma": "build/index.js" - }, - "engines": { - "node": "^20.19 || ^22.12 || >=24.0" - }, - "peerDependencies": { - "better-sqlite3": ">=9.0.0", - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "better-sqlite3": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -6649,25 +6922,6 @@ "react-is": "^16.13.1" } }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/proper-lockfile/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "devOptional": true, - "license": "ISC" - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -6678,23 +6932,6 @@ "node": ">=6" } }, - "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "devOptional": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -6716,17 +6953,6 @@ ], "license": "MIT" }, - "node_modules/rc9": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz", - "integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "defu": "^6.1.6", - "destr": "^2.0.5" - } - }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -6755,18 +6981,18 @@ "dev": true, "license": "MIT" }, - "node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", - "devOptional": true, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, "license": "MIT", - "engines": { - "node": ">= 20.19.0" + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "engines": { + "node": ">=8" } }, "node_modules/reflect.getprototypeof": { @@ -6813,21 +7039,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/remeda": { - "version": "2.33.4", - "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", - "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", - "devOptional": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/remeda" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6877,16 +7093,6 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -6898,6 +7104,40 @@ "node": ">=0.10.0" } }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -6977,12 +7217,18 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true, - "license": "MIT" + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } }, "node_modules/scheduler": { "version": "0.27.0", @@ -7000,12 +7246,6 @@ "semver": "bin/semver.js" } }, - "node_modules/seq-queue": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", - "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", - "devOptional": true - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -7117,7 +7357,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -7130,7 +7370,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7212,17 +7452,26 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "devOptional": true, - "license": "ISC", - "engines": { - "node": ">=14" + "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/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": ">= 10" } }, "node_modules/source-map-js": { @@ -7234,16 +7483,6 @@ "node": ">=0.10.0" } }, - "node_modules/sqlstring": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", - "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -7251,11 +7490,11 @@ "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==", - "devOptional": true, + "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/stop-iteration-iterator": { @@ -7392,7 +7631,20 @@ "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=4" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/strip-json-comments": { @@ -7457,6 +7709,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", @@ -7478,10 +7747,27 @@ "url": "https://opencollective.com/webpack" } }, + "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": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -7526,6 +7812,36 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.2.tgz", + "integrity": "sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.2" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.2.tgz", + "integrity": "sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -7539,6 +7855,42 @@ "node": ">=8.0" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -7679,7 +8031,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -7732,6 +8084,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.2.tgz", + "integrity": "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -7818,26 +8180,288 @@ "punycode": "^2.1.0" } }, - "node_modules/valibot": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", - "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", - "devOptional": true, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, "peerDependencies": { - "typescript": ">=5" + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { - "typescript": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false } } }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest/node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz", + "integrity": "sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "is-plain-object": "^5.0.0", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -7938,6 +8562,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -7948,6 +8589,45 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -7968,17 +8648,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/zeptomatch": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", - "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "grammex": "^3.1.11", - "graphmatch": "^1.1.0" - } - }, "node_modules/zod": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index cf2c2cc..e3cb56e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,9 +2,16 @@ "name": "tanglaw", "version": "0.1.0", "private": true, + "browserslist": [ + "last 2 Chrome versions", + "last 2 Firefox versions", + "last 2 Safari versions", + "last 2 Edge versions" + ], "scripts": { "dev": "next dev", "build": "next build", + "analyze": "ANALYZE=true next build", "start": "next start", "lint": "eslint", "dev:backend": "cd ../backend && npm run dev", @@ -12,26 +19,32 @@ "start:backend": "cd ../backend && npm run start" }, "dependencies": { - "@prisma/client": "^7.8.0", - "@supabase/ssr": "^0.10.3", - "@supabase/supabase-js": "^2.106.2", + "clsx": "^2.1.1", "framer-motion": "^11.0.0", - "lucide-react": "^1.16.0", + "lucide-react": "^0.474.0", "next": "16.2.6", "next-auth": "^4.24.14", + "next-themes": "^0.4.6", "react": "19.2.4", "react-dom": "19.2.4", + "tailwind-merge": "^3.6.0", "zod": "^4.4.3" }, "devDependencies": { + "@next/bundle-analyzer": "^16.2.9", "@tailwindcss/postcss": "^4", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@vitejs/plugin-react": "^6.0.2", "eslint": "^9", "eslint-config-next": "16.2.6", - "prisma": "^7.8.0", + "jsdom": "^29.1.1", "tailwindcss": "^4", - "typescript": "^5" + "typescript": "^5.9.3", + "vitest": "^4.1.8" } } diff --git a/frontend/prisma/schema.prisma b/frontend/prisma/schema.prisma index ee6024a..72982f7 100644 --- a/frontend/prisma/schema.prisma +++ b/frontend/prisma/schema.prisma @@ -34,14 +34,23 @@ enum QuestionType { FILIPINO } +enum AssessmentMode { + DIAGNOSTIC + MOCK +} + model Question { - id String @id @default(uuid()) - type QuestionType - difficulty Int - text String @db.Text - choices Json - correctAnswer String - explanation String @db.Text + id String @id @default(uuid()) + type QuestionType + difficulty Int + assessmentMode AssessmentMode @default(DIAGNOSTIC) + sourceLabel String? + sequenceNo Int @default(0) + isActive Boolean @default(true) + text String @db.Text + choices Json + correctAnswer String + explanation String @db.Text } model User { diff --git a/frontend/public/assets/apple-touch-icon.png b/frontend/public/assets/apple-touch-icon.png new file mode 100644 index 0000000..85593f4 Binary files /dev/null and b/frontend/public/assets/apple-touch-icon.png differ diff --git a/frontend/public/assets/etheral-mask.webp b/frontend/public/assets/etheral-mask.webp new file mode 100644 index 0000000..10ad272 Binary files /dev/null and b/frontend/public/assets/etheral-mask.webp differ diff --git a/frontend/public/assets/etheral-noise.webp b/frontend/public/assets/etheral-noise.webp new file mode 100644 index 0000000..2b198cf Binary files /dev/null and b/frontend/public/assets/etheral-noise.webp differ diff --git a/frontend/public/assets/favicon-16x16.png b/frontend/public/assets/favicon-16x16.png new file mode 100644 index 0000000..dc5f139 Binary files /dev/null and b/frontend/public/assets/favicon-16x16.png differ diff --git a/frontend/public/assets/favicon-32x32.png b/frontend/public/assets/favicon-32x32.png new file mode 100644 index 0000000..a82fbc1 Binary files /dev/null and b/frontend/public/assets/favicon-32x32.png differ diff --git a/frontend/public/assets/favicon-48x48.png b/frontend/public/assets/favicon-48x48.png new file mode 100644 index 0000000..770588a Binary files /dev/null and b/frontend/public/assets/favicon-48x48.png differ diff --git a/tanglaw/frontend/public/assets/owel-full-body.png b/frontend/public/assets/owel-full-body.png similarity index 100% rename from tanglaw/frontend/public/assets/owel-full-body.png rename to frontend/public/assets/owel-full-body.png diff --git a/frontend/public/assets/owel-full-body.webp b/frontend/public/assets/owel-full-body.webp new file mode 100644 index 0000000..7e64397 Binary files /dev/null and b/frontend/public/assets/owel-full-body.webp differ diff --git a/tanglaw/frontend/public/assets/owel-full-body2.0.png b/frontend/public/assets/owel-full-body2.0.png similarity index 100% rename from tanglaw/frontend/public/assets/owel-full-body2.0.png rename to frontend/public/assets/owel-full-body2.0.png diff --git a/frontend/public/assets/owel-full-body2.0.webp b/frontend/public/assets/owel-full-body2.0.webp new file mode 100644 index 0000000..0957152 Binary files /dev/null and b/frontend/public/assets/owel-full-body2.0.webp differ diff --git a/frontend/public/assets/owel-full.png b/frontend/public/assets/owel-full.png deleted file mode 100644 index 8bee488..0000000 Binary files a/frontend/public/assets/owel-full.png and /dev/null differ diff --git a/frontend/public/assets/owel-head-dark.svg b/frontend/public/assets/owel-head-dark.svg new file mode 100644 index 0000000..3523e3c --- /dev/null +++ b/frontend/public/assets/owel-head-dark.svg @@ -0,0 +1,238 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/assets/owel-head.svg b/frontend/public/assets/owel-head.svg new file mode 100644 index 0000000..d4e93ad --- /dev/null +++ b/frontend/public/assets/owel-head.svg @@ -0,0 +1,238 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/assets/owel-head.webp b/frontend/public/assets/owel-head.webp new file mode 100644 index 0000000..eade504 Binary files /dev/null and b/frontend/public/assets/owel-head.webp differ diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000..3e8d6c0 Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/team/albano.jpg b/frontend/public/team/albano.jpg deleted file mode 100644 index bee9f0f..0000000 Binary files a/frontend/public/team/albano.jpg and /dev/null differ diff --git a/tanglaw/frontend/public/team/albano2.0.jpg b/frontend/public/team/albano2.0.jpg similarity index 100% rename from tanglaw/frontend/public/team/albano2.0.jpg rename to frontend/public/team/albano2.0.jpg diff --git a/frontend/public/team/alberto.jpg b/frontend/public/team/alberto.jpg deleted file mode 100644 index f839ff8..0000000 Binary files a/frontend/public/team/alberto.jpg and /dev/null differ diff --git a/tanglaw/frontend/public/team/alberto2.0.jpg b/frontend/public/team/alberto2.0.jpg similarity index 100% rename from tanglaw/frontend/public/team/alberto2.0.jpg rename to frontend/public/team/alberto2.0.jpg diff --git a/frontend/public/team/araullo.jpg b/frontend/public/team/araullo.jpg deleted file mode 100644 index f839ff8..0000000 Binary files a/frontend/public/team/araullo.jpg and /dev/null differ diff --git a/tanglaw/frontend/public/team/araullo2.0.png b/frontend/public/team/araullo2.0.png similarity index 100% rename from tanglaw/frontend/public/team/araullo2.0.png rename to frontend/public/team/araullo2.0.png diff --git a/frontend/public/team/araullo2.0.webp b/frontend/public/team/araullo2.0.webp new file mode 100644 index 0000000..1209ee4 Binary files /dev/null and b/frontend/public/team/araullo2.0.webp differ diff --git a/frontend/public/team/bonador.jpg b/frontend/public/team/bonador.jpg deleted file mode 100644 index f839ff8..0000000 Binary files a/frontend/public/team/bonador.jpg and /dev/null differ diff --git a/tanglaw/frontend/public/team/bonador2.0.jpg b/frontend/public/team/bonador2.0.jpg similarity index 100% rename from tanglaw/frontend/public/team/bonador2.0.jpg rename to frontend/public/team/bonador2.0.jpg diff --git a/frontend/public/team/cruz.jpg b/frontend/public/team/cruz.jpg deleted file mode 100644 index f839ff8..0000000 Binary files a/frontend/public/team/cruz.jpg and /dev/null differ diff --git a/tanglaw/frontend/public/team/cruz2.0.png b/frontend/public/team/cruz2.0.png similarity index 100% rename from tanglaw/frontend/public/team/cruz2.0.png rename to frontend/public/team/cruz2.0.png diff --git a/frontend/public/team/delosreyes.jpg b/frontend/public/team/delosreyes.jpg deleted file mode 100644 index f839ff8..0000000 Binary files a/frontend/public/team/delosreyes.jpg and /dev/null differ diff --git a/tanglaw/frontend/public/team/delosreyes2.0.jpg b/frontend/public/team/delosreyes2.0.jpg similarity index 100% rename from tanglaw/frontend/public/team/delosreyes2.0.jpg rename to frontend/public/team/delosreyes2.0.jpg diff --git a/frontend/public/team/faustino.jpg b/frontend/public/team/faustino.jpg deleted file mode 100644 index f839ff8..0000000 Binary files a/frontend/public/team/faustino.jpg and /dev/null differ diff --git a/tanglaw/frontend/public/team/faustino2.0.png b/frontend/public/team/faustino2.0.png similarity index 100% rename from tanglaw/frontend/public/team/faustino2.0.png rename to frontend/public/team/faustino2.0.png diff --git a/frontend/public/team/faustino2.0.webp b/frontend/public/team/faustino2.0.webp new file mode 100644 index 0000000..eb9ebec Binary files /dev/null and b/frontend/public/team/faustino2.0.webp differ diff --git a/frontend/public/team/madera.jpg b/frontend/public/team/madera.jpg deleted file mode 100644 index f839ff8..0000000 Binary files a/frontend/public/team/madera.jpg and /dev/null differ diff --git a/tanglaw/frontend/public/team/madera2.0.png b/frontend/public/team/madera2.0.png similarity index 100% rename from tanglaw/frontend/public/team/madera2.0.png rename to frontend/public/team/madera2.0.png diff --git a/frontend/public/team/madera2.0.webp b/frontend/public/team/madera2.0.webp new file mode 100644 index 0000000..0a8cfb3 Binary files /dev/null and b/frontend/public/team/madera2.0.webp differ diff --git a/frontend/public/team/pajares.jpg b/frontend/public/team/pajares.jpg deleted file mode 100644 index f839ff8..0000000 Binary files a/frontend/public/team/pajares.jpg and /dev/null differ diff --git a/tanglaw/frontend/public/team/pajares2.0.jpg b/frontend/public/team/pajares2.0.jpg similarity index 100% rename from tanglaw/frontend/public/team/pajares2.0.jpg rename to frontend/public/team/pajares2.0.jpg diff --git a/frontend/public/team/partible.jpg b/frontend/public/team/partible.jpg deleted file mode 100644 index f839ff8..0000000 Binary files a/frontend/public/team/partible.jpg and /dev/null differ diff --git a/tanglaw/frontend/public/team/partible2.0.jpg b/frontend/public/team/partible2.0.jpg similarity index 100% rename from tanglaw/frontend/public/team/partible2.0.jpg rename to frontend/public/team/partible2.0.jpg diff --git a/frontend/public/team/payoyo.jpg b/frontend/public/team/payoyo.jpg deleted file mode 100644 index f839ff8..0000000 Binary files a/frontend/public/team/payoyo.jpg and /dev/null differ diff --git a/frontend/public/team/payoyo2.0.jpg b/frontend/public/team/payoyo2.0.jpg new file mode 100644 index 0000000..77519ca Binary files /dev/null and b/frontend/public/team/payoyo2.0.jpg differ diff --git a/frontend/public/team/perez.jpg b/frontend/public/team/perez.jpg deleted file mode 100644 index f839ff8..0000000 Binary files a/frontend/public/team/perez.jpg and /dev/null differ diff --git a/tanglaw/frontend/public/team/perez2.0.png b/frontend/public/team/perez2.0.png similarity index 100% rename from tanglaw/frontend/public/team/perez2.0.png rename to frontend/public/team/perez2.0.png diff --git a/frontend/public/team/perez2.0.webp b/frontend/public/team/perez2.0.webp new file mode 100644 index 0000000..164e553 Binary files /dev/null and b/frontend/public/team/perez2.0.webp differ diff --git a/frontend/public/team/salvaloza.jpg b/frontend/public/team/salvaloza.jpg deleted file mode 100644 index f839ff8..0000000 Binary files a/frontend/public/team/salvaloza.jpg and /dev/null differ diff --git a/tanglaw/frontend/public/team/salvaloza2.0.png b/frontend/public/team/salvaloza2.0.png similarity index 100% rename from tanglaw/frontend/public/team/salvaloza2.0.png rename to frontend/public/team/salvaloza2.0.png diff --git a/frontend/public/team/salvaloza2.0.webp b/frontend/public/team/salvaloza2.0.webp new file mode 100644 index 0000000..4db4421 Binary files /dev/null and b/frontend/public/team/salvaloza2.0.webp differ diff --git a/frontend/src/__e2e__/public-pages.spec.ts b/frontend/src/__e2e__/public-pages.spec.ts new file mode 100644 index 0000000..df9652a --- /dev/null +++ b/frontend/src/__e2e__/public-pages.spec.ts @@ -0,0 +1,117 @@ +import { test, expect } from "@playwright/test"; + +test.describe("TANGLAW Landing Page", () => { + test("loads the landing page successfully", async ({ page }) => { + await page.goto("/"); + await expect(page).toHaveTitle(/TANGLAW/); + await expect(page.locator("text=TANGLAW").first()).toBeVisible(); + }); + + test("has navigation links", async ({ page }) => { + await page.goto("/"); + await expect(page.locator("text=Home").first()).toBeVisible(); + await expect(page.locator("text=About").first()).toBeVisible(); + await expect(page.locator("text=Contact").first()).toBeVisible(); + }); +}); + +test.describe("Login Flow", () => { + test("navigates to login page", async ({ page }) => { + await page.goto("/login"); + await expect(page.locator("text=Welcome back, scholar")).toBeVisible(); + await expect(page.locator("text=Sign in to your dashboard")).toBeVisible(); + }); + + test("shows login form fields", async ({ page }) => { + await page.goto("/login"); + await expect(page.locator('input[type="email"]')).toBeVisible(); + await expect(page.locator('input[type="password"]')).toBeVisible(); + await expect(page.locator('button[type="submit"]')).toBeVisible(); + }); + + test("shows error for empty form submission", async ({ page }) => { + await page.goto("/login"); + // Remove HTML5 required attributes so the JS validation runs instead + await page.evaluate(() => { + document.querySelectorAll('input[required]').forEach(el => el.removeAttribute('required')); + }); + await page.locator('button[type="submit"]').click(); + await expect(page.locator("text=Please fill out all required fields.")).toBeVisible(); + }); + + test("shows error for invalid credentials", async ({ page }) => { + await page.goto("/login"); + await page.locator('input[type="email"]').fill("invalid@test.com"); + await page.locator('input[type="password"]').fill("wrongpassword"); + await page.locator('button[type="submit"]').click(); + // Wait for error message — the error appears in a dedicated message container + // with ShieldAlert icon and error border styling + await expect(page.locator('svg.lucide-shield-alert')).toBeVisible({ timeout: 10000 }); + }); +}); + +test.describe("Signup Flow", () => { + test("navigates to signup page from login", async ({ page }) => { + await page.goto("/login"); + await page.locator("text=Create an account").click(); + await expect(page).toHaveURL(/\/signup/); + await expect(page.locator("text=Register as a scholar")).toBeVisible(); + }); + + test("shows signup form fields", async ({ page }) => { + await page.goto("/signup"); + await expect(page.locator('input[type="text"]')).toBeVisible(); + await expect(page.locator('input[type="email"]')).toBeVisible(); + await expect(page.locator('input[type="password"]')).toBeVisible(); + await expect(page.locator('button[type="submit"]')).toBeVisible(); + }); +}); + +test.describe("About Page", () => { + test("loads the about page", async ({ page }) => { + await page.goto("/about"); + await expect(page.locator("text=Redefining scholarship navigation")).toBeVisible(); + }); +}); + +test.describe("Contact Page", () => { + test("loads the contact page", async ({ page }) => { + await page.goto("/contact"); + await expect(page.locator("text=PUP Manila")).toBeVisible(); + }); +}); + +test.describe("Mobile Menu", () => { + test("opens and closes mobile menu on landing page", async ({ page }) => { + await page.setViewportSize({ width: 375, height: 812 }); + await page.goto("/"); + + // Open menu + const menuButton = page.locator('button[aria-label="Open navigation menu"]'); + await expect(menuButton).toBeVisible(); + await menuButton.click(); + + // Wait for the mobile dropdown transition to complete (200ms CSS transition) + await page.waitForTimeout(300); + + // Menu should be visible — the mobile dropdown renders Home links AFTER the desktop nav + // in the DOM, so .last() picks the visible mobile dropdown link + await expect(page.locator("text=Home").last()).toBeVisible(); + + // Close menu — use force:true since the backdrop may intercept pointer events + const closeButton = page.locator('button[aria-label="Close navigation menu"]'); + await closeButton.click({ force: true }); + }); + + test("mobile menu closes on backdrop click", async ({ page }) => { + await page.setViewportSize({ width: 375, height: 812 }); + await page.goto("/"); + + const menuButton = page.locator('button[aria-label="Open navigation menu"]'); + await menuButton.click(); + + // Click backdrop + const backdrop = page.locator('[aria-hidden="true"]').first(); + await backdrop.click(); + }); +}); diff --git a/frontend/src/__e2e__/verify-particles.spec.ts b/frontend/src/__e2e__/verify-particles.spec.ts new file mode 100644 index 0000000..7c3bdd7 --- /dev/null +++ b/frontend/src/__e2e__/verify-particles.spec.ts @@ -0,0 +1,121 @@ +import { test, expect } from "@playwright/test"; + +import type { Page, ConsoleMessage } from "@playwright/test"; + +function collectConsoleErrors(page: Page): string[] { + const consoleErrors: string[] = []; + page.on("console", (msg: ConsoleMessage) => { + if (msg.type() === "error") consoleErrors.push(msg.text()); + }); + page.on("pageerror", (err: Error) => consoleErrors.push(err.message)); + return consoleErrors; +} + +function assertNoTsParticlesErrors(consoleErrors: string[]) { + const tsParticlesErrors = consoleErrors.filter(e => + e.toLowerCase().includes("tsparticles") || + e.toLowerCase().includes("particlesprovider") + ); + expect(tsParticlesErrors).toEqual([]); +} + +test("tsParticles background is visible and interactive", async ({ page }) => { + const consoleErrors = collectConsoleErrors(page); + + // Enable test mode to bypass performance guards (idle timer, IntersectionObserver) + await page.addInitScript(() => { + (window as any).__TEST_MODE__ = true; + }); + + await page.goto("/", { waitUntil: "networkidle", timeout: 30000 }); + await page.waitForTimeout(3000); // allow particles to initialize + + // Wait for the particles canvas to be created (ParticlesProvider is async) + const container = page.locator("#tanglaw-particles"); + await container.waitFor({ state: "visible", timeout: 20000 }); + + // Check that canvas was created inside the div + const canvas = page.locator("#tanglaw-particles canvas"); + await expect(canvas).toBeVisible(); + + // Test hover interactivity (mouse move should not error) + await page.mouse.move(400, 300); + await page.waitForTimeout(500); + + // Test click interactivity + await page.mouse.click(400, 300); + await page.waitForTimeout(500); + + // Take screenshot for visual inspection (not a baseline comparison) + await page.screenshot({ path: "/tmp/particles-screenshot.png", fullPage: true }); + + assertNoTsParticlesErrors(consoleErrors); +}); + +test.describe("visual regression", () => { + test.setTimeout(60000); + + test("tsParticles background switches correctly between light and dark themes", async ({ page }) => { + const consoleErrors = collectConsoleErrors(page); + + // Enable test mode to bypass performance guards (idle timer, IntersectionObserver) + await page.addInitScript(() => { + (window as any).__TEST_MODE__ = true; + }); + + await page.goto("/", { waitUntil: "networkidle", timeout: 30000 }); + await page.waitForTimeout(3000); // allow particles to initialize + + // Verify particles are visible in light mode (default) + const container = page.locator("#tanglaw-particles"); + await expect(container).toBeVisible({ timeout: 15000 }); + const canvas = page.locator("#tanglaw-particles canvas"); + await expect(canvas).toBeVisible(); + + // Verify initial theme is light + const html = page.locator("html"); + await expect(html).toHaveAttribute("class", /light/); + + // Take light mode screenshot and compare to baseline + // Mask the particle canvas so random positions don't cause false positives + await expect(page).toHaveScreenshot("particles-light.png", { + fullPage: true, + mask: [page.locator("#tanglaw-particles")], + maxDiffPixels: 100, + threshold: 0.05, + timeout: 30000, + }); + + // Click the theme toggle button to switch to dark mode + const themeToggle = page.locator("button[aria-label='Switch to dark theme']"); + await expect(themeToggle).toBeVisible(); + await themeToggle.click(); + + // Wait for the theme to transition and particles to re-render + await page.waitForTimeout(2000); + + // Verify the html class now contains 'dark' + await expect(html).toHaveAttribute("class", /dark/); + + // Verify particles are still visible in dark mode + await expect(container).toBeVisible(); + await expect(canvas).toBeVisible(); + + // Take dark mode screenshot and compare to baseline + await expect(page).toHaveScreenshot("particles-dark.png", { + fullPage: true, + mask: [page.locator("#tanglaw-particles")], + maxDiffPixels: 100, + threshold: 0.05, + timeout: 30000, + }); + + // Toggle back to light mode + await page.locator("button[aria-label='Switch to light theme']").click(); + await page.waitForTimeout(1000); + await expect(html).toHaveAttribute("class", /light/); + await expect(container).toBeVisible(); + + assertNoTsParticlesErrors(consoleErrors); + }); +}); diff --git a/frontend/src/__e2e__/verify-particles.spec.ts-snapshots/particles-dark-chromium-linux.png b/frontend/src/__e2e__/verify-particles.spec.ts-snapshots/particles-dark-chromium-linux.png new file mode 100644 index 0000000..2295d8d Binary files /dev/null and b/frontend/src/__e2e__/verify-particles.spec.ts-snapshots/particles-dark-chromium-linux.png differ diff --git a/frontend/src/__e2e__/verify-particles.spec.ts-snapshots/particles-light-chromium-linux.png b/frontend/src/__e2e__/verify-particles.spec.ts-snapshots/particles-light-chromium-linux.png new file mode 100644 index 0000000..9a23732 Binary files /dev/null and b/frontend/src/__e2e__/verify-particles.spec.ts-snapshots/particles-light-chromium-linux.png differ diff --git a/frontend/src/__tests__/readiness-feedback.test.tsx b/frontend/src/__tests__/readiness-feedback.test.tsx new file mode 100644 index 0000000..d49a9d9 --- /dev/null +++ b/frontend/src/__tests__/readiness-feedback.test.tsx @@ -0,0 +1,200 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import React from "react"; + +// Mock framer-motion +vi.mock("framer-motion", () => ({ + motion: { + div: ({ children, ...props }: Record) =>
{children as React.ReactNode}
, + span: ({ children, ...props }: Record) => {children as React.ReactNode}, + button: ({ children, ...props }: Record) => , + }, + AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}, +})); + +// Mock lucide-react icons +vi.mock("lucide-react", () => ({ + Award: () => Award, + CheckCircle2: () => CheckCircle2, + AlertTriangle: () => AlertTriangle, + BookMarked: () => BookMarked, + RotateCcw: () => RotateCcw, +})); + +import ReadinessFeedback from "../components/readiness-feedback"; + +type SubjectType = "Mathematics" | "Science" | "English" | "Filipino" | "Logical Reasoning"; + +const defaultSubjectScores: Record = { + Mathematics: { correct: 8, total: 10, answered: 10 }, + Science: { correct: 6, total: 10, answered: 10 }, + English: { correct: 9, total: 10, answered: 10 }, + Filipino: { correct: 7, total: 10, answered: 10 }, + "Logical Reasoning": { correct: 5, total: 10, answered: 10 }, +}; + +const defaultReadinessDetails = { + level: "Moderately Ready", + color: "#f59e0b", + icon: Icon, + text: "You have a solid foundation but need to strengthen a few key areas. Focus on the subjects where you scored below 70%.", +}; + +const defaultStudyRecommendations = [ + "Review logical reasoning patterns and syllogisms", + "Practice more science problem sets", + "Focus on Filipino grammar rules", +]; + +describe("ReadinessFeedback", () => { + const defaultProps = { + score: 35, + total: 50, + scorePercentage: 70, + subjectScores: defaultSubjectScores, + readinessDetails: defaultReadinessDetails, + studyRecommendations: defaultStudyRecommendations, + onRestart: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + // ─── Happy Path Tests ────────────────────────────────────────────── + + it("renders the readiness analysis header", () => { + render(); + expect(screen.getByText("Readiness Check Analysis")).toBeInTheDocument(); + }); + + it("displays the cumulative score", () => { + render(); + expect(screen.getByText("35")).toBeInTheDocument(); + expect(screen.getByText("/ 50")).toBeInTheDocument(); + }); + + it("displays the score percentage", () => { + render(); + expect(screen.getByText("(70% accuracy)")).toBeInTheDocument(); + }); + + it("renders the readiness summary level", () => { + render(); + expect(screen.getByText("Moderately Ready")).toBeInTheDocument(); + }); + + it("renders the readiness details text", () => { + render(); + expect(screen.getByText(/solid foundation/)).toBeInTheDocument(); + }); + + it("renders all subject breakdowns", () => { + render(); + expect(screen.getByText("Mathematics")).toBeInTheDocument(); + expect(screen.getByText("Science")).toBeInTheDocument(); + expect(screen.getByText("English")).toBeInTheDocument(); + expect(screen.getByText("Filipino")).toBeInTheDocument(); + expect(screen.getByText("Logical Reasoning")).toBeInTheDocument(); + }); + + it("renders study recommendations", () => { + render(); + expect(screen.getByText(/logical reasoning patterns/)).toBeInTheDocument(); + expect(screen.getByText(/science problem sets/)).toBeInTheDocument(); + expect(screen.getByText(/Filipino grammar rules/)).toBeInTheDocument(); + }); + + it("calls onRestart when restart button is clicked", async () => { + const onRestart = vi.fn(); + const user = userEvent.setup(); + render(); + + await user.click(screen.getByText("Start New Assessment Check")); + expect(onRestart).toHaveBeenCalledOnce(); + }); + + it("renders the Award icon", () => { + render(); + expect(screen.getByTestId("icon-award")).toBeInTheDocument(); + }); + + // ─── Edge Case Tests ─────────────────────────────────────────────── + + it("renders with a perfect score", () => { + render( + + ); + expect(screen.getByText("50")).toBeInTheDocument(); + expect(screen.getByText("(100% accuracy)")).toBeInTheDocument(); + expect(screen.getByText("Fully Ready")).toBeInTheDocument(); + }); + + it("renders with a zero score", () => { + render( + + ); + expect(screen.getByText("0")).toBeInTheDocument(); + expect(screen.getByText("(0% accuracy)")).toBeInTheDocument(); + }); + + it("renders with empty study recommendations", () => { + render(); + expect(screen.getByText("Recommended Study Areas:")).toBeInTheDocument(); + }); + + it("renders with no study recommendations list items", () => { + render(); + // The heading should still be visible, but no list items + expect(screen.getByText("Recommended Study Areas:")).toBeInTheDocument(); + }); + + it("skips subjects with zero total questions", () => { + const scoresWithEmpty = { + ...defaultSubjectScores, + Filipino: { correct: 0, total: 0, answered: 0 }, + }; + render( + + ); + // Filipino should NOT be rendered since total is 0 + expect(screen.getByText("Mathematics")).toBeInTheDocument(); + expect(screen.getByText("Science")).toBeInTheDocument(); + }); + + // ─── Display State Tests ────────────────────────────────────────── + + it("renders the TANGLAW subtitle banner", () => { + render(); + expect( + screen.getByText("TANGLAW Scholarship Competency Analyzer") + ).toBeInTheDocument(); + }); + + it("renders restart button with correct text", () => { + render(); + const restartButton = screen.getByText("Start New Assessment Check"); + expect(restartButton).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/__tests__/readiness-question.test.tsx b/frontend/src/__tests__/readiness-question.test.tsx new file mode 100644 index 0000000..072659c --- /dev/null +++ b/frontend/src/__tests__/readiness-question.test.tsx @@ -0,0 +1,183 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import React from "react"; + +// Mock framer-motion +vi.mock("framer-motion", () => ({ + motion: { + div: ({ children, ...props }: Record) =>
{children as React.ReactNode}
, + span: ({ children, ...props }: Record) => {children as React.ReactNode}, + button: ({ children, ...props }: Record) => , + }, + AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}, +})); + +// Mock lucide-react icons +vi.mock("lucide-react", () => ({ + Timer: (props: Record) => Timer, +})); + +import ReadinessQuestion from "../components/readiness-question"; + +const mockQuestion = { + id: 1, + subject: "Mathematics" as const, + difficulty: 1, + questionText: "What is the square root of 144?", + options: ["10", "11", "12", "13"], + correctAnswer: 2, +}; + +describe("ReadinessQuestion", () => { + const defaultProps = { + question: mockQuestion, + questionIndex: 0, + totalQuestions: 10, + selectedAnswer: undefined as number | undefined, + onSelectOption: vi.fn(), + onNext: vi.fn(), + onPrev: vi.fn(), + timeLeft: 45, + canGoNext: true, + canGoPrev: false, + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + // ─── Happy Path Tests ────────────────────────────────────────────── + + it("renders the question text", () => { + render(); + expect(screen.getByText("What is the square root of 144?")).toBeInTheDocument(); + }); + + it("renders all 4 answer options", () => { + render(); + expect(screen.getByText("10")).toBeInTheDocument(); + expect(screen.getByText("11")).toBeInTheDocument(); + expect(screen.getByText("12")).toBeInTheDocument(); + expect(screen.getByText("13")).toBeInTheDocument(); + }); + + it("renders the question counter", () => { + render(); + expect(screen.getByText("Question 1 of 10")).toBeInTheDocument(); + }); + + it("renders the subject label", () => { + render(); + expect(screen.getByText("Mathematics")).toBeInTheDocument(); + }); + + it("renders the timer display", () => { + render(); + expect(screen.getByText("45s")).toBeInTheDocument(); + }); + + it("calls onSelectOption when an option is clicked", async () => { + const onSelectOption = vi.fn(); + const user = userEvent.setup(); + render(); + + await user.click(screen.getByText("12")); + expect(onSelectOption).toHaveBeenCalledWith(2); + }); + + it("calls onNext when Next button is clicked", async () => { + const onNext = vi.fn(); + const user = userEvent.setup(); + render(); + + await user.click(screen.getByText("Next Item")); + expect(onNext).toHaveBeenCalledOnce(); + }); + + it("shows Finish Assessment for last question", () => { + render( + + ); + expect(screen.getByText("Finish Assessment")).toBeInTheDocument(); + }); + + // ─── Edge Case Tests ─────────────────────────────────────────────── + + it("disables Previous button on first question", () => { + render(); + expect(screen.getByText("Previous")).toBeDisabled(); + }); + + it("disables Next button when canGoNext is false", () => { + render(); + expect(screen.getByText("Next Item")).toBeDisabled(); + }); + + it("highlights selected answer with correct styles", async () => { + const user = userEvent.setup(); + render(); + + // Selected option should exist + const selected = screen.getByText("12"); + expect(selected).toBeInTheDocument(); + }); + + it("renders timer in red when time is low (< 10s)", () => { + render(); + expect(screen.getByText("5s")).toBeInTheDocument(); + }); + + it("displays correct progress for middle question", () => { + render(); + expect(screen.getByText("Question 5 of 10")).toBeInTheDocument(); + }); + + it("handles all subject types", () => { + const subjects = ["Mathematics", "Science", "English", "Filipino", "Logical Reasoning"] as const; + subjects.forEach((subject) => { + const { unmount } = render( + + ); + expect(screen.getByText(subject)).toBeInTheDocument(); + unmount(); + }); + }); + + // ─── Error / Boundary Tests ──────────────────────────────────────── + + it("renders with no answer selected (undefined)", () => { + render(); + // All options should be visible and clickable + expect(screen.getByText("10")).toBeInTheDocument(); + expect(screen.getByText("11")).toBeInTheDocument(); + }); + + it("handles zero time left gracefully", () => { + render(); + expect(screen.getByText("0s")).toBeInTheDocument(); + }); + + it("renders long question text without breaking", () => { + render( + + ); + expect( + screen.getByText(/very long question text/) + ).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/__tests__/readiness-setup.test.tsx b/frontend/src/__tests__/readiness-setup.test.tsx new file mode 100644 index 0000000..720e52d --- /dev/null +++ b/frontend/src/__tests__/readiness-setup.test.tsx @@ -0,0 +1,190 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import React from "react"; + +// Mock framer-motion +vi.mock("framer-motion", () => ({ + motion: { + div: ({ children, ...props }: Record) =>
{children as React.ReactNode}
, + span: ({ children, ...props }: Record) => {children as React.ReactNode}, + button: ({ children, ...props }: Record) => , + }, + AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}, +})); + +// Mock lucide-react icons +vi.mock("lucide-react", () => ({ + Play: () => Play, + Check: () => Check, + BookOpen: () => BookOpen, + ArrowRight: () => ArrowRight, + Loader2: () => Loader2, +})); + +import ReadinessSetup, { SUBJECTS } from "../components/readiness-setup"; +import type { SubjectType } from "../components/readiness-setup"; + +describe("ReadinessSetup", () => { + const defaultProps = { + selectedSubjects: [] as SubjectType[], + onSubjectChange: vi.fn(), + itemCount: 10 as const, + onItemCountChange: vi.fn(), + selectedDifficulty: 3 as const, + onDifficultyChange: vi.fn(), + isLoading: false, + loadError: null as string | null, + onStartDiagnostics: vi.fn(), + onStartMockExam: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + // ─── Happy Path Tests ────────────────────────────────────────────── + + it("renders the setup form title", () => { + render(); + expect(screen.getByText("Option 1: Gamified Quick Diagnostics")).toBeInTheDocument(); + }); + + it("renders all 5 subject checkboxes", () => { + render(); + SUBJECTS.forEach((subject) => { + expect(screen.getByText(subject)).toBeInTheDocument(); + }); + }); + + it("renders difficulty selection buttons", () => { + render(); + expect(screen.getByText("Lvl 1 · Easiest")).toBeInTheDocument(); + expect(screen.getByText("Lvl 3 · Moderate")).toBeInTheDocument(); + expect(screen.getByText("Lvl 5 · Advanced")).toBeInTheDocument(); + }); + + it("calls onSubjectChange when a subject is clicked", async () => { + const onSubjectChange = vi.fn(); + const user = userEvent.setup(); + render(); + + await user.click(screen.getByText("Mathematics")); + expect(onSubjectChange).toHaveBeenCalledWith("Mathematics"); + }); + + it("calls onDifficultyChange when a difficulty is clicked", async () => { + const onDifficultyChange = vi.fn(); + const user = userEvent.setup(); + render(); + + await user.click(screen.getByText("Lvl 1 · Easiest")); + expect(onDifficultyChange).toHaveBeenCalledWith(1); + }); + + it("calls onStartDiagnostics when Start Diagnostics button is clicked", async () => { + const onStartDiagnostics = vi.fn(); + const user = userEvent.setup(); + render(); + + await user.click(screen.getByText("Start Diagnostics Check")); + expect(onStartDiagnostics).toHaveBeenCalledOnce(); + }); + + it("calls onStartMockExam when Launch Full Mock Exam button is clicked", async () => { + const onStartMockExam = vi.fn(); + const user = userEvent.setup(); + render(); + + await user.click(screen.getByText("Launch Full Mock Exam")); + expect(onStartMockExam).toHaveBeenCalledOnce(); + }); + + it("shows selected subjects with check icon", () => { + const selectedSubjects: SubjectType[] = ["Mathematics", "Science"]; + render(); + + // Subject buttons should exist + expect(screen.getByText("Mathematics")).toBeInTheDocument(); + expect(screen.getByText("Science")).toBeInTheDocument(); + }); + + it("renders the range slider for item count", () => { + render(); + const slider = screen.getByRole("slider"); + expect(slider).toBeInTheDocument(); + expect(slider).toHaveValue("10"); + }); + + it("renders the mock exam option card", () => { + render(); + expect(screen.getByText("Option 2: Comprehensive Mock Exam")).toBeInTheDocument(); + }); + + // ─── Edge Case Tests ─────────────────────────────────────────────── + + it("renders correctly with no subjects selected", () => { + render(); + // No check icons should be rendered + const checks = screen.queryAllByTestId("icon-check"); + expect(checks.length).toBe(0); + }); + + it("renders correctly with all subjects selected", () => { + const allSubjects: SubjectType[] = [...SUBJECTS]; + render(); + // All 5 check icons should be rendered + const checks = screen.queryAllByTestId("icon-check"); + expect(checks.length).toBe(5); + }); + + it("renders with minimum item count (10)", () => { + render(); + const slider = screen.getByRole("slider"); + expect(slider).toHaveValue("10"); + }); + + it("renders with maximum item count (50)", () => { + render(); + const slider = screen.getByRole("slider"); + expect(slider).toHaveValue("50"); + }); + + it("calls onItemCountChange when slider value changes", () => { + const onItemCountChange = vi.fn(); + render(); + + fireEvent.change(screen.getByRole("slider"), { target: { value: "20" } }); + expect(onItemCountChange).toHaveBeenCalledWith(20); + }); + + it("renders mock exam statistics correctly", () => { + render(); + expect(screen.getByText("Fixed 250 items total")).toBeInTheDocument(); + expect(screen.getByText("Exactly 50 items per subject")).toBeInTheDocument(); + expect(screen.getByText("3 Hours countdown timer")).toBeInTheDocument(); + }); + + // ─── Display State Tests ────────────────────────────────────────── + + it("highlights selected difficulty as active", () => { + render(); + + // All difficulty buttons exist + expect(screen.getByText("Lvl 1 · Easiest")).toBeInTheDocument(); + expect(screen.getByText("Lvl 3 · Moderate")).toBeInTheDocument(); + expect(screen.getByText("Lvl 5 · Advanced")).toBeInTheDocument(); + }); + + it("displays correct item count label", () => { + render(); + expect(screen.getByText("Selected: 20 Items")).toBeInTheDocument(); + }); + + it("has accessible subject selection buttons", () => { + render(); + const buttons = screen.getAllByRole("button"); + // Should have 5 subject buttons + 5 difficulty buttons + 2 launch buttons + tick marks + expect(buttons.length).toBeGreaterThanOrEqual(10); + }); +}); diff --git a/frontend/src/__tests__/scholarship-browser.test.tsx b/frontend/src/__tests__/scholarship-browser.test.tsx new file mode 100644 index 0000000..add1e14 --- /dev/null +++ b/frontend/src/__tests__/scholarship-browser.test.tsx @@ -0,0 +1,201 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import React from "react"; + +// Mock next/dynamic to render components synchronously in tests +vi.mock("next/dynamic", () => ({ + default: (importFn: () => Promise<{ default: React.ComponentType }>, opts?: { loading?: () => React.ReactNode }) => { + const DynamicComponent = React.lazy(importFn); + return Object.assign( + (props: Record) => ( + + + + ), + { displayName: "DynamicComponent" } + ); + }, +})); + +// Mock lucide-react icons +vi.mock("lucide-react", () => ({ + Clock: () => Clock, + Flag: () => Flag, + Timer: () => Timer, + Play: () => Play, + Check: () => Check, + BookOpen: () => BookOpen, + ArrowRight: () => ArrowRight, + Award: () => Award, + CheckCircle2: () => CheckCircle2, + AlertTriangle: () => AlertTriangle, + BookMarked: () => BookMarked, + RotateCcw: () => RotateCcw, + ChevronDown: () => ChevronDown, + ChevronUp: () => ChevronUp, + Search: () => Search, + SlidersHorizontal: () => Sliders, + DollarSign: () => Dollar, + Building2: () => Building, + ExternalLink: () => ExternalLink, + GraduationCap: () => GradCap, + Filter: () => Filter, + FileText: () => FileText, + HelpCircle: () => HelpCircle, + Calendar: () => Calendar, + ArrowUp: () => ArrowUp, + ChevronLeft: () => ChevronLeft, + ChevronRight: () => ChevronRight, + X: () => X, + Menu: () => Menu, + Send: () => Send, + MessageCircle: () => MessageCircle, + Sparkles: () => Sparkles, + LogOut: () => LogOut, + ShieldCheck: () => ShieldCheck, + LogIn: () => LogIn, + UserPlus: () => UserPlus, + ShieldAlert: () => ShieldAlert, + ArrowLeft: () => ArrowLeft, + Database: () => Database, + MapPin: () => MapPin, + User: () => User, + Linkedin: () => Linkedin, + Users: () => Users, + Mail: () => Mail, + Moon: () => Moon, + Sun: () => Sun, + GripHorizontal: () => Grip, +})); + +// Mock next-themes +vi.mock("next-themes", () => ({ + useTheme: () => ({ resolvedTheme: "light", setTheme: vi.fn() }), + ThemeProvider: ({ children }: { children: React.ReactNode }) => <>{children}, +})); + +// Mock next/image +vi.mock("next/image", () => ({ + default: ({ src, alt, width, height, className }: Record) => ( + {alt + ), +})); + +// Mock next/navigation +vi.mock("next/navigation", () => ({ + useRouter: () => ({ push: vi.fn(), replace: vi.fn() }), + usePathname: () => "/dashboard", +})); + +// Mock next-auth +vi.mock("next-auth/react", () => ({ + useSession: () => ({ status: "authenticated", data: { user: { email: "test@test.com" } } }), + signOut: vi.fn(), + signIn: vi.fn(), +})); + +// Mock framer-motion +vi.mock("framer-motion", () => ({ + motion: { + div: ({ children, ...props }: Record) =>
{children as React.ReactNode}
, + span: ({ children, ...props }: Record) => {children as React.ReactNode}, + button: ({ children, ...props }: Record) => , + section: ({ children, ...props }: Record) =>
{children as React.ReactNode}
, + article: ({ children, ...props }: Record) =>
{children as React.ReactNode}
, + header: ({ children, ...props }: Record) =>
{children as React.ReactNode}
, + }, + AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}, +})); + +import ScholarshipBrowser from "../components/scholarship-browser"; + +// Mock the dynamic scholarship data import +vi.mock("@/data/scholarships-data", () => ({ + SCHOLARSHIPS_DATA: [ + { + name: "Test Scholarship 1", + provider: "Test Provider 1", + coverageType: "Full Coverage", + classification: "Public", + strand: "All Strand", + overview: "Test overview 1 for scholarship testing.", + coverageDetails: "Test coverage details", + eligibility: { + nationality: "Filipino", + }, + priorityPrograms: ["BS Computer Science"], + requirements: ["Requirement 1"], + examInformation: { type: "None" }, + deadline: "December 2026", + links: ["https://test.com"], + }, + { + name: "Test Scholarship 2", + provider: "Test Provider 2", + coverageType: "Partial Coverage", + classification: "Private", + strand: "STEM", + overview: "Test overview 2 for scholarship testing.", + coverageDetails: "Test coverage details 2", + eligibility: { + minimumGPA: "2.00", + }, + priorityPrograms: ["BS Information Technology"], + requirements: ["Requirement 2"], + examInformation: { type: "Interview" }, + deadline: "January 2027", + links: ["https://test2.com"], + }, + ], +})); + +describe("ScholarshipBrowser", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders the filter panel and main display area", async () => { + render(); + + // Should show loading skeleton initially + await waitFor(() => { + const filterHeaders = screen.getAllByText("Filter Controls"); + expect(filterHeaders.length).toBeGreaterThanOrEqual(1); + }); + }); + + it("shows scholarship cards after data loads", async () => { + render(); + + await waitFor(() => { + expect(screen.getByText("Test Scholarship 1")).toBeInTheDocument(); + }, { timeout: 3000 }); + }); + + it("shows matching opportunities count after load", async () => { + render(); + + await waitFor(() => { + expect(screen.getByText("2")).toBeInTheDocument(); + }); + }); + + it("shows empty state when no scholarships match", async () => { + render(); + + await waitFor(() => { + // Type a search that won't match anything + const searchInput = screen.getByPlaceholderText(/Search DOST, Megaworld/); + expect(searchInput).toBeInTheDocument(); + }); + }); + + it("renders filter controls", async () => { + render(); + + await waitFor(() => { + expect(screen.getByPlaceholderText(/Search DOST, Megaworld/)).toBeInTheDocument(); + }); + }); +}); diff --git a/frontend/src/__tests__/scholarship-card.test.tsx b/frontend/src/__tests__/scholarship-card.test.tsx new file mode 100644 index 0000000..34ee7be --- /dev/null +++ b/frontend/src/__tests__/scholarship-card.test.tsx @@ -0,0 +1,135 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen, waitFor, fireEvent } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import ScholarshipCard from "../components/scholarship-card"; +import type { ScholarshipOpportunity } from "@/data/scholarships-data"; + +const mockScholarship: ScholarshipOpportunity = { + name: "DOST-SEI Undergraduate Scholarship", + provider: "Department of Science and Technology", + coverageType: "Full Coverage", + classification: "National Government", + strand: "STEM", + overview: "The DOST-SEI Undergraduate Scholarship is a national government scholarship program.", + coverageDetails: "Full Tuition & school fees coverage up to ₱40,000/yr", + eligibility: { + nationality: "Natural-born Filipino citizen", + minimumGPA: "GWA of 85% or higher", + }, + priorityPrograms: ["BS Computer Science", "BS Information Technology", "BS Engineering"], + requirements: ["Natural-born Filipino citizen", "GWA of 85% or higher", "Must pass the DOST-SEI exam"], + examInformation: { type: "Examination" }, + deadline: "Subject to the annual DOST-SEI application cycle", + links: ["https://www.sei.dost.gov.ph"], +}; + +describe("ScholarshipCard", () => { + it("renders scholarship name and provider", () => { + render( + {}} + /> + ); + + expect(screen.getByText("DOST-SEI Undergraduate Scholarship")).toBeInTheDocument(); + expect(screen.getByText(/Department of Science and Technology/)).toBeInTheDocument(); + }); + + it("shows overview text", () => { + render( + {}} + /> + ); + + expect(screen.getByText(/The DOST-SEI Undergraduate Scholarship is a national government scholarship program/)).toBeInTheDocument(); + }); + + it("does not show expanded details when collapsed", () => { + render( + {}} + /> + ); + + expect(screen.queryByText("Benefits & Coverage")).not.toBeInTheDocument(); + expect(screen.getByText("View Full Details")).toBeInTheDocument(); + }); + + it("shows expanded details when isExpanded is true", () => { + render( + {}} + /> + ); + + expect(screen.getByText("Benefits & Coverage")).toBeInTheDocument(); + expect(screen.getByText("Eligibility Criteria")).toBeInTheDocument(); + expect(screen.getByText("Hide Details")).toBeInTheDocument(); + }); + + it("calls onToggle when toggle button is clicked", async () => { + const handleToggle = vi.fn(); + const user = userEvent.setup(); + + render( + + ); + + await user.click(screen.getByText("View Full Details")); + expect(handleToggle).toHaveBeenCalledWith("DOST-SEI Undergraduate Scholarship"); + }); + + it("shows eligibility details in expanded view", () => { + render( + {}} + /> + ); + + const elements = screen.getAllByText(/Natural-born Filipino citizen/); + expect(elements.length).toBeGreaterThanOrEqual(1); + const gpaElements = screen.getAllByText(/GWA of 85% or higher/); + expect(gpaElements.length).toBeGreaterThanOrEqual(1); + }); + + it("renders priority programs as badges", () => { + render( + {}} + /> + ); + + expect(screen.getByText("BS Computer Science")).toBeInTheDocument(); + expect(screen.getByText("BS Information Technology")).toBeInTheDocument(); + expect(screen.getByText("BS Engineering")).toBeInTheDocument(); + }); + + it("renders classification tag correctly for government scholarship", () => { + render( + {}} + /> + ); + + expect(screen.getByText("National Government")).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/__tests__/scholarship-filter-panel.test.tsx b/frontend/src/__tests__/scholarship-filter-panel.test.tsx new file mode 100644 index 0000000..05bb638 --- /dev/null +++ b/frontend/src/__tests__/scholarship-filter-panel.test.tsx @@ -0,0 +1,110 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import ScholarshipFilterPanel from "../components/scholarship-filter-panel"; + +describe("ScholarshipFilterPanel", () => { + const defaultProps = { + searchTerm: "", + onSearchChange: vi.fn(), + incomeLimit: "all", + onIncomeChange: vi.fn(), + scholarshipType: "all", + onTypeChange: vi.fn(), + programType: "all", + onProgramChange: vi.fn(), + showMobileFilters: true, + onToggleMobile: vi.fn(), + onReset: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders search input", () => { + render(); + expect(screen.getByPlaceholderText(/Search DOST, Megaworld/)).toBeInTheDocument(); + }); + + it("calls onSearchChange when typing in search input", async () => { + const onSearchChange = vi.fn(); + const user = userEvent.setup(); + render(); + + await user.type(screen.getByPlaceholderText(/Search DOST, Megaworld/), "DOST"); + expect(onSearchChange).toHaveBeenCalled(); + }); + + it("calls onIncomeChange when changing income dropdown", () => { + const onIncomeChange = vi.fn(); + render(); + + fireEvent.change(screen.getByDisplayValue("Any Income Bracket"), { + target: { value: "400000" }, + }); + expect(onIncomeChange).toHaveBeenCalledWith("400000"); + }); + + it("calls onReset when clear all is clicked", async () => { + const onReset = vi.fn(); + const user = userEvent.setup(); + render(); + + const clearButtons = screen.getAllByText("Clear All"); + await user.click(clearButtons[0]); + expect(onReset).toHaveBeenCalled(); + }); + + it("renders income bracket options", () => { + render(); + + expect(screen.getByText("Any Income Bracket")).toBeInTheDocument(); + expect(screen.getByText("₱400,000 or below")).toBeInTheDocument(); + expect(screen.getByText("₱350,000 or below")).toBeInTheDocument(); + }); + + it("renders sponsoring type buttons", () => { + render(); + + expect(screen.getByText("all")).toBeInTheDocument(); + expect(screen.getByText("public")).toBeInTheDocument(); + expect(screen.getByText("private")).toBeInTheDocument(); + }); + + it("calls onTypeChange when clicking a sponsoring type button", async () => { + const onTypeChange = vi.fn(); + const user = userEvent.setup(); + render(); + + await user.click(screen.getByText("private")); + expect(onTypeChange).toHaveBeenCalledWith("private"); + }); + + it("renders academic stream radio options", () => { + render(); + + expect(screen.getByText("All Programs / Any")).toBeInTheDocument(); + expect(screen.getByText("STEM Courses")).toBeInTheDocument(); + expect(screen.getByText("Humanities / Arts")).toBeInTheDocument(); + }); + + it("calls onToggleMobile when mobile toggle is clicked", async () => { + const onToggleMobile = vi.fn(); + const user = userEvent.setup(); + render(); + + const filterHeaders = screen.getAllByText("Filter Controls"); + await user.click(filterHeaders[0]); + expect(onToggleMobile).toHaveBeenCalled(); + }); + + it("does not show filter body when showMobileFilters is false", () => { + render(); + + // The filter body should be in the DOM (rendered but hidden via CSS) + // The parent div has `hidden` class when showMobileFilters is false + const filterBody = screen.getByPlaceholderText(/Search DOST, Megaworld/).closest(".space-y-6"); + expect(filterBody).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/__tests__/scholarship-pagination.test.tsx b/frontend/src/__tests__/scholarship-pagination.test.tsx new file mode 100644 index 0000000..a4cfd21 --- /dev/null +++ b/frontend/src/__tests__/scholarship-pagination.test.tsx @@ -0,0 +1,55 @@ +import { describe, it, expect } from "vitest"; +import { render, screen } from "@testing-library/react"; +import ScholarshipPagination from "../components/scholarship-pagination"; + +describe("ScholarshipPagination", () => { + it("renders current page and total pages", () => { + render( + {}} + /> + ); + + const pageText = screen.getByText(/Page.*of/); + expect(pageText).toBeInTheDocument(); + }); + + it("disables Previous button on first page", () => { + render( + {}} + /> + ); + + expect(screen.getByText("Previous")).toBeDisabled(); + }); + + it("disables Next button on last page", () => { + render( + {}} + /> + ); + + expect(screen.getByText("Next")).toBeDisabled(); + }); + + it("enables both buttons on middle page", () => { + render( + {}} + /> + ); + + expect(screen.getByText("Previous")).not.toBeDisabled(); + expect(screen.getByText("Next")).not.toBeDisabled(); + }); +}); diff --git a/frontend/src/__tests__/setup.ts b/frontend/src/__tests__/setup.ts new file mode 100644 index 0000000..f149f27 --- /dev/null +++ b/frontend/src/__tests__/setup.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom/vitest"; diff --git a/frontend/src/app/(auth)/layout.tsx b/frontend/src/app/(auth)/layout.tsx new file mode 100644 index 0000000..5ebb800 --- /dev/null +++ b/frontend/src/app/(auth)/layout.tsx @@ -0,0 +1,11 @@ +"use client"; + +import NextAuthProvider from "@/components/NextAuthProvider"; + +export default function AuthLayout({ + children, +}: { + children: React.ReactNode; +}) { + return {children}; +} diff --git a/frontend/src/app/(auth)/login/page.tsx b/frontend/src/app/(auth)/login/page.tsx index b26b17c..8ea80db 100644 --- a/frontend/src/app/(auth)/login/page.tsx +++ b/frontend/src/app/(auth)/login/page.tsx @@ -4,21 +4,36 @@ * Login page for the public authentication flow. * Uses local storage to simulate an authenticated session. */ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import Link from "next/link"; -import { useRouter } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import { signIn } from "next-auth/react"; import { LogIn, ArrowRight, CheckCircle2, ShieldAlert } from "lucide-react"; import { motion } from "framer-motion"; +import dynamic from "next/dynamic"; +import { GlowingText } from "../../../../components/ui/glowing-text"; +import { loginUser } from "@/lib/backend"; + +const EtheralShadow = dynamic( + () => import("../../../../components/ui/etheral-shadow").then((mod) => mod.EtheralShadow), + { ssr: false } +); export default function LoginPage() { const router = useRouter(); + const searchParams = useSearchParams(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [loading, setLoading] = useState(false); const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null); const [showOAuth, setShowOAuth] = useState(false); + useEffect(() => { + if (searchParams.get("expired") === "1") { + setMessage({ type: "error", text: "Your session has expired. Please sign in again to continue." }); + } + }, [searchParams]); + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!email || !password) { @@ -29,29 +44,44 @@ export default function LoginPage() { setLoading(true); setMessage(null); - const result = await signIn("credentials", { - redirect: false, - email, - password, - }); - - if (result?.error) { - setMessage({ type: "error", text: result.error }); - } else { - router.push("/dashboard"); + try { + await loginUser(email, password); + const result = await signIn("credentials", { + redirect: false, + email, + password, + }); + + if (result?.error) { + setMessage({ type: "error", text: result.error }); + } else { + router.push("/dashboard"); + } + } catch (error) { + setMessage({ + type: "error", + text: error instanceof Error ? error.message : "Unable to sign in.", + }); + } finally { + setLoading(false); } - - setLoading(false); }; return ( -
+
+
-
+
+

Scholarship Sanctuary

- Welcome back, scholar. + Welcome back, scholar.

Enter your credentials to continue exploring scholarships, readiness checks, and the Owel guidance environment. @@ -69,7 +99,9 @@ export default function LoginPage() {

Secure Scholar Login

-

Sign in to your dashboard

+

+ Sign in to your dashboard +

{message && ( @@ -91,7 +123,7 @@ export default function LoginPage() {
)} -
+
-

+

New to TANGLAW?{' '} - - Create an account + + + Create an account +

+
); diff --git a/frontend/src/app/(auth)/signup/page.tsx b/frontend/src/app/(auth)/signup/page.tsx index b3acd4a..6e048e8 100644 --- a/frontend/src/app/(auth)/signup/page.tsx +++ b/frontend/src/app/(auth)/signup/page.tsx @@ -9,6 +9,13 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; import { UserPlus, ArrowLeft, CheckCircle2, ShieldAlert } from "lucide-react"; import { motion } from "framer-motion"; +import dynamic from "next/dynamic"; +import { GlowingText } from "../../../../components/ui/glowing-text"; + +const EtheralShadow = dynamic( + () => import("../../../../components/ui/etheral-shadow").then((mod) => mod.EtheralShadow), + { ssr: false } +); import { signupAccount } from "@/lib/backend"; import { signIn } from "next-auth/react"; @@ -55,13 +62,20 @@ export default function SignupPage() { }; return ( -
+
+
-
+
+

New Scholar Portal

- Start your TANGLAW journey. + Start your TANGLAW journey.

Create your student account and gain access to personalized scholarship matching, exam review tools, and the Owel learning companion. @@ -77,9 +91,11 @@ export default function SignupPage() {

-
+

Create your account

-

Register as a scholar

+

+ Register as a scholar +

{message && ( @@ -101,7 +117,7 @@ export default function SignupPage() {
)} -
+
-

+

Already a scholar?{' '} - - Log In + + + Log In +

+
); diff --git a/frontend/src/app/(main)/readiness/page.tsx b/frontend/src/app/(main)/readiness/page.tsx deleted file mode 100644 index ca5c1df..0000000 --- a/frontend/src/app/(main)/readiness/page.tsx +++ /dev/null @@ -1,30 +0,0 @@ -"use client"; - -/** - * Public redirect page for the branded readiness route. - * Sends unauthenticated visitors to the login page. - */ -import { useEffect } from "react"; -import { useRouter } from "next/navigation"; - -export default function ReadinessPage() { - const router = useRouter(); - - useEffect(() => { - router.replace("/login"); - }, [router]); - - return ( -
-
-

Readiness check moved to the secure dashboard

-

- The readiness assessment is only available through the authenticated Scholar Hub. Please log in to continue. -

-
- Redirecting to login... -
-
-
- ); -} diff --git a/frontend/src/app/(main)/scholarships/page.tsx b/frontend/src/app/(main)/scholarships/page.tsx deleted file mode 100644 index 2e8e05f..0000000 --- a/frontend/src/app/(main)/scholarships/page.tsx +++ /dev/null @@ -1,29 +0,0 @@ -"use client"; - -/** - * Public placeholder page that redirects users to login when they try to access scholarship content. - */ -import { useEffect } from "react"; -import { useRouter } from "next/navigation"; - -export default function ScholarshipsPage() { - const router = useRouter(); - - useEffect(() => { - router.replace("/login"); - }, [router]); - - return ( -
-
-

Scholarship workspace moved to the secure dashboard

-

- The scholarship directory is now accessible exclusively after authentication. Sign in to continue to your secure Scholar Hub. -

-
- Redirecting to login... -
-
-
- ); -} diff --git a/tanglaw/frontend/src/app/about/page.tsx b/frontend/src/app/about/about-client.tsx similarity index 56% rename from tanglaw/frontend/src/app/about/page.tsx rename to frontend/src/app/about/about-client.tsx index e112d3d..1f95bd3 100644 --- a/tanglaw/frontend/src/app/about/page.tsx +++ b/frontend/src/app/about/about-client.tsx @@ -4,12 +4,17 @@ * About page describing the project goals and team members. */ import Image from "next/image"; -import { Users, BookOpen, Linkedin, ChevronLeft, ChevronRight } from "lucide-react"; -import { useRef, useState, useEffect, useCallback } from "react"; -import { motion } from "framer-motion"; +import { Users, BookOpen, Linkedin } from "lucide-react"; +import { useState } from "react"; +import dynamic from "next/dynamic"; import ScrollReveal from "@/components/scroll-reveal"; -import { EtheralShadow } from "../../../components/ui/etheral-shadow"; import { GlowingText } from "../../../components/ui/glowing-text"; +import CarouselSection from "@/components/carousel-section"; + +const EtheralShadow = dynamic( + () => import("../../../components/ui/etheral-shadow").then((mod) => mod.EtheralShadow), + { ssr: false } +); const TEAM_DESCRIPTION = "A student-led research initiative that blends academic insight with scholarship navigation. We built TANGLAW to make grants easier to find, understand, and act on."; @@ -141,267 +146,30 @@ const BARRIERS = [ // Map of usernames to their 2.0 photo filenames (mixed .jpg/.png) const PHOTO_2_0: Record = { - salvaloza: "salvaloza2.0.png", + salvaloza: "salvaloza2.0.webp", bonador: "bonador2.0.jpg", - madera: "madera2.0.png", + madera: "madera2.0.webp", alberto: "alberto2.0.jpg", partible: "partible2.0.jpg", - perez: "perez2.0.png", - araullo: "araullo2.0.png", + perez: "perez2.0.webp", + araullo: "araullo2.0.webp", pajares: "pajares2.0.jpg", payoyo: "payoyo2.0.jpg", delosreyes: "delosreyes2.0.jpg", - faustino: "faustino2.0.png", + faustino: "faustino2.0.webp", cruz: "cruz2.0.png", albano: "albano2.0.jpg", }; -const PILLARS = [ - { - number: "01", - title: "Guided Scholarship Matching", - description: "TANGLAW turns raw grant criteria into student-friendly matches and decision prompts.", - }, - { - number: "02", - title: "Adaptive Readiness Check", - description: "Interactive drills help students identify strengths, gaps, and high-impact review areas.", - }, - { - number: "03", - title: "AI Navigation Companion", - description: "Owel answers eligibility questions, simplifies terms, and recommends next steps.", - }, - { - number: "04", - title: "Smart Scholarship Directory", - description: "Filter grants by institution, funder type, and requirement intensity in one interface.", - }, - { - number: "05", - title: "Review Engine & Analytics", - description: "Practice modules and completion metrics keep learners motivated and accountable.", - }, -]; - -function CarouselSection() { - const carouselRef = useRef(null); - const [activeIndex, setActiveIndex] = useState(0); - const activeIndexRef = useRef(0); - const autoPlayRef = useRef | null>(null); - const pauseUntilRef = useRef(0); - const isProgrammaticScroll = useRef(false); - - const getCardWidth = useCallback(() => { - if (!carouselRef.current?.children[0]) return 364; - return (carouselRef.current.children[0] as HTMLElement).offsetWidth + 24; - }, []); - - const scrollTo = useCallback((index: number) => { - if (!carouselRef.current) return; - isProgrammaticScroll.current = true; - const cardWidth = getCardWidth(); - carouselRef.current.scrollTo({ - left: index * cardWidth, - behavior: "smooth", - }); - activeIndexRef.current = index; - setActiveIndex(index); - }, [getCardWidth]); - - const nextSlide = useCallback(() => { - const next = (activeIndexRef.current + 1) % PILLARS.length; - scrollTo(next); - }, [scrollTo]); - - const prevSlide = useCallback(() => { - const prev = (activeIndexRef.current - 1 + PILLARS.length) % PILLARS.length; - scrollTo(prev); - }, [scrollTo]); - - // Sync activeIndex on manual scroll - const handleScroll = useCallback(() => { - if (!carouselRef.current) return; - const { scrollLeft } = carouselRef.current; - const cardWidth = getCardWidth(); - if (cardWidth <= 0) return; - const idx = Math.round(scrollLeft / cardWidth); - if (idx !== activeIndexRef.current && idx >= 0 && idx < PILLARS.length) { - activeIndexRef.current = idx; - setActiveIndex(idx); - } - }, [getCardWidth]); +export default function AboutClient() { + const [activeMember, setActiveMember] = useState(null); - // Pause auto-play briefly on any user interaction - const pauseAutoPlay = useCallback(() => { - pauseUntilRef.current = Date.now() + 8000; - }, []); + const toggleMember = (username: string) => { + setActiveMember((prev) => (prev === username ? null : username)); + }; - // Auto-play interval: advances every 4s unless paused - useEffect(() => { - autoPlayRef.current = setInterval(() => { - if (Date.now() >= pauseUntilRef.current) { - nextSlide(); - } - }, 4000); - return () => { - if (autoPlayRef.current) clearInterval(autoPlayRef.current); - }; - }, [nextSlide]); - - // Scroll listener for dot syncing and pausing auto-play - useEffect(() => { - const el = carouselRef.current; - if (!el) return; - const onScroll = () => { - handleScroll(); - // Only pause auto-play for user-initiated scrolls (drag/wheel/touch), - // not for programmatic scrolls from auto-play itself. - if (!isProgrammaticScroll.current) { - pauseAutoPlay(); - } - isProgrammaticScroll.current = false; - }; - el.addEventListener("scroll", onScroll, { passive: true }); - return () => el.removeEventListener("scroll", onScroll); - }, [handleScroll, pauseAutoPlay]); - - return ( -
- -

Our solution

-

The five pillars of TANGLAW

-
- -
- {/* Slider Track */} -
- {PILLARS.map((pillar, index) => ( - { if (e.key === "Enter" || e.key === " ") { pauseAutoPlay(); scrollTo(index); }}} - initial={{ opacity: 0, y: 40 }} - whileInView={{ opacity: 1, y: 0 }} - viewport={{ once: true, margin: "-80px" }} - transition={{ - type: "spring", - stiffness: 100, - damping: 20, - delay: 0.08 * index, - }} - className=" - snap-start shrink-0 grow-0 basis-[340px] - min-h-[470px] - rounded-[1.5rem] - border border-white/10 - bg-[color:var(--theme-surface)]/90 - px-8 py-10 - shadow-2xl shadow-black/20 - transition-all duration-300 - cursor-pointer - flex flex-col - hover:-translate-y-1 hover:border-white/20 - " - onClick={() => { pauseAutoPlay(); scrollTo(index); }} - > - {/* Large numeric indicator */} - - {pillar.number} - - - {/* Divider spacer */} -
- - {/* Title */} -

- {pillar.title} -

- - {/* Body text */} -

- {pillar.description} -

- - ))} -
-
- - {/* Bottom Controls — centered arrow buttons */} -
- - - {/* Pill indicator dots */} -
- {PILLARS.map((pillar, index) => ( -
- - -
-
- ); -} - -export default function AboutPage() { return ( -
+
{DOCUMENTATION_TEAM.map((member, idx) => ( -
-
+
+
toggleMember(member.username)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + toggleMember(member.username); + } + }} + > {member.name} - {/* LinkedIn overlay — fades in on card hover or keyboard focus */} -
+ {/* LinkedIn overlay — click to toggle on mobile, hover or click on desktop */} + + {/* LinkedIn tap hint badge — visible on mobile only */} +

{member.role}

{member.name}

@@ -536,8 +320,20 @@ export default function AboutPage() {
{DEVELOPMENT_TEAM.map((member, idx) => ( -
-
+
+
toggleMember(member.username)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + toggleMember(member.username); + } + }} + > {member.name} - {/* LinkedIn overlay — fades in on card hover or keyboard focus */} -
+ {/* LinkedIn overlay — click to toggle on mobile, hover or click on desktop */} + + {/* LinkedIn tap hint badge — visible on mobile only */} +

{member.role}

{member.name}

diff --git a/frontend/src/app/about/page.tsx b/frontend/src/app/about/page.tsx index a9b05dd..19690e4 100644 --- a/frontend/src/app/about/page.tsx +++ b/frontend/src/app/about/page.tsx @@ -1,327 +1,19 @@ -"use client"; +import dynamic from "next/dynamic"; -/** - * About page describing the project goals and team members. - */ -import Image from "next/image"; -import { Users, BookOpen, Code, GraduationCap, ChevronLeft, ChevronRight } from "lucide-react"; -import { useRef } from "react"; - -const TEAM_DESCRIPTION = "A student-led research initiative that blends academic insight with scholarship navigation. We built TANGLAW to make grants easier to find, understand, and act on."; - -const DOCUMENTATION_TEAM = [ - { - name: "Godsent John C. Salvaloza", - role: "Documentation Head", - description: "Oversees all paper sections, references indexation, and final compiled academic paper validation.", - username: "salvaloza", - }, - { - name: "Rhaine Venice B. Bonador", - role: "Introduction Writer", - description: "Handles Chapter 1 problem contexts, general socioeconomic gaps, and solution frameworks.", - username: "bonador", - }, - { - name: "Kyle Ashley B. Madera", - role: "Statement of the Problem Writer", - description: "Transforms operational goals into measurable research questions, metrics, and study definitions.", - username: "madera", - }, - { - name: "Hannah Mae V. Alberto", - role: "RRL Lead Writer", - description: "Manages literature review synthesis, source curation, and academic narrative alignment.", - username: "alberto", - }, - { - name: "Hannah Nicole B. Partible", - role: "RRL Assistant & Citation Checker", - description: "Maintains reference accuracy, citation formatting, and academic consistency.", - username: "partible", - }, - { - name: "Emerald T. Perez", - role: "Methodology Writer", - description: "Structures the research design, evaluation method, and analytical process.", - username: "perez", - }, - { - name: "Julliane Mae G. Araullo", - role: "Results Writer", - description: "Compiles findings, performance metrics, and usability impact narratives.", - username: "araullo", - }, - { - name: "Daniel F. Pajares", - role: "Discussion Writer", - description: "Explains implications, limitations, and future recommendations of the project.", - username: "pajares", - }, -]; - -const DEVELOPMENT_TEAM = [ - { - name: "Bennett P. Payoyo", - role: "Project Manager", - description: "Directs operational scope, research alignment, task delegation, and final deployment quality gates.", - username: "payoyo", - }, - { - name: "An-joe Mikael T. Albano", - role: "Frontend Developer", - description: "Leads interface delivery, motion polish, and responsive behavior.", - username: "albano", - }, - { - name: "Levrone Viel S. Delos Reyes", - role: "Frontend & QA", - description: "Supports UI quality checks, interaction validation, and accessibility review.", - username: "delosreyes", - }, - { - name: "Charles Joseph V. Faustino", - role: "Backend Developer & Database Manager", - description: "Builds server interactions, data flow structure, and simulated persistence pathways.", - username: "faustino", - }, - { - name: "Justin Angelo G. Cruz", - role: "QA Tester / Technical Documentation", - description: "Manages test matrices, documentation clarity, and final feature verification.", - username: "cruz", - }, -]; - -const STATISTICS = [ - { - label: "30.5%", - description: "of Grade 3 learners show basic reading proficiency.", - }, - { - label: "0.47%", - description: "of Grade 12 learners demonstrate grade-level readiness.", - }, -]; - -const BARRIERS = [ - { - title: "Sensory Overload", - description: - "Traditional scholarship research is noisy, fragmented, and difficult for students who need clear, structured guidance.", - }, - { - title: "Executive Dysfunction", - description: - "Learners struggle to convert requirements into action when they lack step-by-step application support.", - }, - { - title: "Resource Gap", - description: - "Many students lack access to verified grant sources, eligibility summaries, and coaching tools in one place.", - }, -]; - -const PILLARS = [ - { - number: "01", - title: "Guided Scholarship Matching", - description: "TANGLAW turns raw grant criteria into student-friendly matches and decision prompts.", - }, - { - number: "02", - title: "Adaptive Readiness Check", - description: "Interactive drills help students identify strengths, gaps, and high-impact review areas.", - }, - { - number: "03", - title: "AI Navigation Companion", - description: "Owel answers eligibility questions, simplifies terms, and recommends next steps.", - }, - { - number: "04", - title: "Smart Scholarship Directory", - description: "Filter grants by institution, funder type, and requirement intensity in one interface.", - }, - { - number: "05", - title: "Review Engine & Analytics", - description: "Practice modules and completion metrics keep learners motivated and accountable.", - }, -]; - -function CarouselSection() { - const carouselRef = useRef(null); - - const scroll = (direction: "left" | "right") => { - if (carouselRef.current) { - const scrollAmount = 320; - carouselRef.current.scrollBy({ - left: direction === "left" ? -scrollAmount : scrollAmount, - behavior: "smooth", - }); - } - }; - - return ( -
-
-

Our solution

-

The five pillars of TANGLAW

-
-
-
- {PILLARS.map((pillar) => ( -
-
- {pillar.number} -
-

{pillar.title}

-

{pillar.description}

-
- ))} +const AboutClient = dynamic(() => import("./about-client"), { + loading: () => ( +
+
+
+
+
+
- - - -
-
- ); -} +
+ ), +}); export default function AboutPage() { - return ( -
-
-
-
-
- - Who Builds TANGLAW -
-

- Redefining scholarship navigation for every learner. -

-

- {TEAM_DESCRIPTION} -

-
- -
-
-
- Our mission -
-

A navigation sanctuary for scholarship-ready students.

-

- TANGLAW is designed to simplify grant discovery and preparation through a single dashboard, with clear pathways that help learners move from confusion to confidence. -

-
- -
- {STATISTICS.map((stat) => ( -
-

{stat.label}

-

{stat.description}

-
- ))} -
-
- -
-
-

The Barriers to Brilliance

-

What students face today

-
-
- {BARRIERS.map((item) => ( -
-

{item.title}

-

{item.description}

-
- ))} -
-
- - - -
-
-
-

Our creators

-

Student researchers and builders behind TANGLAW

-

- This group blends documentation, UX, development, and evaluation expertise to create a scholarship platform that works for Filipino learners. -

-
- -
-
-

Documentation Team

-
- {DOCUMENTATION_TEAM.map((member) => ( -
-
-
- {member.name - .split(" ") - .map((part) => part[0]) - .slice(0, 2) - .join("")} -
-
-

{member.role}

-

{member.name}

-

{member.description}

-
- ))} -
-
- -
-

Development Team

-
- {DEVELOPMENT_TEAM.map((member) => ( -
-
-
- {member.name - .split(" ") - .map((part) => part[0]) - .slice(0, 2) - .join("")} -
-
-

{member.role}

-

{member.name}

-

{member.description}

-
- ))} -
-
-
-
-
-
-
- ); + return ; } diff --git a/frontend/src/app/contact/page.tsx b/frontend/src/app/contact/page.tsx index 05cda65..bdaa43a 100644 --- a/frontend/src/app/contact/page.tsx +++ b/frontend/src/app/contact/page.tsx @@ -6,6 +6,14 @@ */ import React, { useState } from "react"; import { Mail, MapPin, Building2, Send, CheckCircle2 } from "lucide-react"; +import dynamic from "next/dynamic"; +import ScrollReveal from "@/components/scroll-reveal"; +import { GlowingText } from "../../../components/ui/glowing-text"; + +const EtheralShadow = dynamic( + () => import("../../../components/ui/etheral-shadow").then((mod) => mod.EtheralShadow), + { ssr: false } +); export default function ContactPage() { const [name, setName] = useState(""); @@ -20,141 +28,153 @@ export default function ContactPage() { if (!name || !email || !messageText) { alert("Please fill out the required fields (Name, Email, Message)."); return; - } - - setLoading(true); - setTimeout(() => { + } setLoading(true); + setTimeout(() => { setLoading(false); setSubmitted(true); setName(""); setGroup(""); setEmail(""); setMessageText(""); - }, 1200); + }, 600); }; return ( -
+
+
-
-
- Contact TANGLAW -
-

- Let us guide your next scholarship move. -

-

- Have questions on scholarship criteria, research data, or the TANGLAW experience? Send a message and our team will respond shortly. -

-
+ +
+
+ Contact TANGLAW +
+

+ Let us guide your next scholarship move. +

+

+ Have questions on scholarship criteria, research data, or the TANGLAW experience? Send a message and our team will respond shortly. +

+
+
-
-
-
- Support Node -
-

Academic Sponsorship

-
-
- -
-

Polytechnic University of the Philippines

-

Anonas St., Santa Mesa, Manila, Metro Manila 1016

-
+ +
+
+
+ Support Node
-
- -
-

Department of Computer Science

-

College of Computer and Information Sciences

-

BSCS 1-2 (STS Research Class)

+

+ Academic Sponsorship +

+
+
+ +
+

Polytechnic University of the Philippines

+

Anonas St., Santa Mesa, Manila, Metro Manila 1016

+
+
+
+ +
+

Department of Computer Science

+

College of Computer and Information Sciences

+

BSCS 1-2 (STS Research Class)

+
-
-
+
+ -
-
-
-

Reach Out

-

Message the TANGLAW team

-
-
- Response within 2 business days + +
+
+
+

Reach Out

+

Message the TANGLAW team

+
+
+ Response within 2 business days +
-
- {submitted && ( -
-
- -
-

Message received.

-

Your note is now queued for review by our student research team.

+ {submitted && ( +
+
+ +
+

Message received.

+

Your note is now queued for review by our student research team.

+
-
- )} + )} + +
+
+ + +
- -
+