commit c784d0779657e83271210b052e31791dbd374196 Author: Omair Saleh Date: Wed Mar 18 18:26:24 2026 +0800 Initial commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..576a934 --- /dev/null +++ b/.env.example @@ -0,0 +1,21 @@ +# Database +DATABASE_URL=postgresql://dokploy:PASSWORD@localhost:5432/ngotoolkit + +# OpenAI +OPENAI_API_KEY=sk-... +OPENAI_MODEL=gpt-4o + +# Auth0 +AUTH0_SECRET=long-random-string +AUTH0_BASE_URL=http://localhost:3000 +AUTH0_ISSUER_BASE_URL=https://YOUR_TENANT.auth0.com +AUTH0_CLIENT_ID= +AUTH0_CLIENT_SECRET= + +# LemonSqueezy +LEMONSQUEEZY_API_KEY= +LEMONSQUEEZY_STORE_ID= +LEMONSQUEEZY_WEBHOOK_SECRET= + +# App +NEXT_PUBLIC_APP_URL=http://localhost:3000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..37f6153 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.next/ +.env +*.tsbuildinfo diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a7263fc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM oven/bun:1 AS deps +WORKDIR /app +COPY package.json bun.lock* ./ +RUN bun install --frozen-lockfile + +FROM oven/bun:1 AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN bun run build + +FROM node:20-slim AS runner +WORKDIR /app +ENV NODE_ENV=production +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static +COPY --from=builder /app/public* ./public/ +EXPOSE 3000 +CMD ["node", "server.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..39bca82 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# NGO Toolkit Lab + +AI-powered grant writing tools for NGOs. 4 tools: Logframe Generator, Funder Rewriter, SMART Indicators, Theory of Change. + +## Setup + +```bash +# Install dependencies +bun install + +# Copy env and fill in values +cp .env.example .env + +# Run database migration +bun run db:migrate + +# Start dev server +bun dev +``` + +## Environment Variables + +See `.env.example` for all required variables: +- `DATABASE_URL` — PostgreSQL connection string +- `OPENAI_API_KEY` — OpenAI API key +- `AUTH0_*` — Auth0 configuration for admin routes +- `LEMONSQUEEZY_*` — Payment processing + +## Database + +Schema is in `lib/db/schema.sql`. Run manually or use the migration script: +```bash +psql $DATABASE_URL < lib/db/schema.sql +# or +bun run db:migrate +``` + +## Docker + +```bash +docker compose up -d +``` + +## Routes + +| Route | Description | +|-------|-------------| +| `/` | Hub — 4 tool cards | +| `/logframe` | Logframe Generator | +| `/funder-rewriter` | Funder Rewriter | +| `/smart-indicators` | SMART Indicators | +| `/theory-of-change` | Theory of Change | +| `/admin` | Dashboard (Auth0 protected) | +| `/admin/config` | Configuration editor | +| `/admin/leads` | Leads table | + +## API Routes + +- `POST /api/[tool]/generate-draft` — Generate initial draft +- `POST /api/[tool]/improve-section` — Improve a section (paid) +- `POST /api/[tool]/validate` — Quality assessment (paid) +- `POST /api/[tool]/finalize` — Produce final version (paid) +- `POST /api/payment/create-checkout` — Create LemonSqueezy checkout +- `POST /api/payment/webhook` — LemonSqueezy webhook handler +- `GET/POST /api/admin/config` — Admin config management +- `GET /api/admin/leads` — Paginated leads +- `GET /api/admin/metrics` — Dashboard metrics +- `POST /api/export/[format]` — Export (docx/excel/pdf/copy) diff --git a/app/[tool]/page.tsx b/app/[tool]/page.tsx new file mode 100644 index 0000000..138535b --- /dev/null +++ b/app/[tool]/page.tsx @@ -0,0 +1,149 @@ +'use client'; + +import { useState } from 'react'; +import { useParams } from 'next/navigation'; +import { tools } from '@/lib/tools'; +import ToolForm from '@/components/ToolForm'; +import OutputPanel from '@/components/OutputPanel'; +import { ToolName } from '@/lib/types'; + +export default function ToolPage() { + const params = useParams(); + const slug = params.tool as string; + const tool = tools.find((t) => t.slug === slug); + + const [sessionId, setSessionId] = useState(null); + const [preview, setPreview] = useState(null); + const [fullOutput, setFullOutput] = useState(null); + const [isPaid, setIsPaid] = useState(false); + const [generating, setGenerating] = useState(false); + const [paymentLoading, setPaymentLoading] = useState(false); + const [email, setEmail] = useState(''); + + if (!tool) { + return ( +
+

Tool not found

+
+ ); + } + + const handleGenerate = async (input: Record) => { + setGenerating(true); + setPreview(null); + setSessionId(null); + try { + const res = await fetch(`/api/${slug}/generate-draft`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ input, email: email || undefined }), + }); + const data = await res.json(); + setSessionId(data.session_id); + setPreview(data.preview); + setIsPaid(data.is_paid); + } catch (e) { + console.error('Generate failed:', e); + } finally { + setGenerating(false); + } + }; + + const handlePayment = async () => { + if (!sessionId) return; + setPaymentLoading(true); + try { + const res = await fetch('/api/payment/create-checkout', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ session_id: sessionId, email }), + }); + const data = await res.json(); + if (data.checkout_url) { + window.location.href = data.checkout_url; + } + } catch (e) { + console.error('Payment failed:', e); + } finally { + setPaymentLoading(false); + } + }; + + // Check if returning from payment (poll session status) + const checkPaymentStatus = async () => { + if (!sessionId) return; + const res = await fetch(`/api/${slug}/validate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ session_id: sessionId }), + }); + if (res.status !== 402) { + setIsPaid(true); + // Fetch full output + const exportRes = await fetch('/api/export/copy', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ session_id: sessionId }), + }); + const data = await exportRes.json(); + if (data.text) setFullOutput(data.text); + } + }; + + return ( +
+
+

+ {tool.icon} {tool.name} +

+

{tool.description}

+
+ +
+ {/* Left: Form */} +
+
+
+ + setEmail(e.target.value)} + placeholder="your@email.com" + className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-accent/50 focus:border-accent" + /> +
+ +
+
+ + {/* Right: Output */} +
+ + {sessionId && !isPaid && ( + + )} +
+
+
+ ); +} diff --git a/app/about/page.tsx b/app/about/page.tsx new file mode 100644 index 0000000..7bfa146 --- /dev/null +++ b/app/about/page.tsx @@ -0,0 +1,92 @@ +const pillars = [ + { title: 'Speed without shortcuts', desc: 'Professional-grade outputs in minutes. No templates, no copy-paste — real structured generation.' }, + { title: 'Built for practitioners', desc: 'Every feature exists because a grant writer asked for it. No feature exists because a PM thought it was clever.' }, + { title: 'Accessible by default', desc: 'Free tier forever. No signup wall. If you\'re writing grants for a small NGO, you shouldn\'t need a budget line to access tools.' }, +]; + +const stats = [ + { value: '120+', label: 'Organisations served' }, + { value: '30', label: 'Countries' }, + { value: '4', label: 'Professional-grade tools' }, + { value: '<2 min', label: 'Average document time' }, +]; + +export default function AboutPage() { + return ( + <> + {/* Hero */} +
+
+

About

+

We build tools for people who write grants.

+

+ NGO Toolkit Lab exists because grant writing is important work — and the tooling around it has been stuck in 2005. +

+
+
+ + {/* Founder note */} +
+
+
+

From the founder

+
+

+ I spent years watching talented programme managers burn weekends on logframes and theories of change — documents that should take hours, not days. The knowledge was there. The sector expertise was there. What was missing was tooling that respected their time. +

+

+ NGO Toolkit Lab started as a logframe generator I built for a friend submitting a DFID proposal. She finished in twenty minutes instead of three days. That felt worth pursuing. +

+

+ We're a small team. We ship tools that work. If something's broken or missing, tell us — we fix things fast. +

+
+

Omair

+

Founder, NGO Toolkit Lab

+
+
+
+ + {/* Mission pillars */} +
+
+

What we believe

+
+ {pillars.map((p, i) => ( +
+

{p.title}

+

{p.desc}

+
+ ))} +
+
+
+ + {/* By the numbers */} +
+
+

By the numbers

+
+ {stats.map((s, i) => ( +
+

{s.value}

+

{s.label}

+
+ ))} +
+
+
+ + {/* CTA */} +
+
+

Try the tools. Judge the work.

+

No pitch deck. No demo call. Just open a tool and see what it builds.

+ + Try free → + +
+
+ + ); +} diff --git a/app/admin/config/page.tsx b/app/admin/config/page.tsx new file mode 100644 index 0000000..2e0a825 --- /dev/null +++ b/app/admin/config/page.tsx @@ -0,0 +1,77 @@ +'use client'; + +import { useUser } from '@auth0/nextjs-auth0/client'; +import { useEffect, useState } from 'react'; +import Link from 'next/link'; + +const CONFIG_LABELS: Record = { + price_amount: 'Price (in pence/cents)', + price_currency: 'Currency', + free_tier_enabled: 'Free Tier Enabled', + max_improvements: 'Max Improvements per Session', + openai_model: 'OpenAI Model', + lemonsqueezy_variant_id: 'LemonSqueezy Variant ID', +}; + +export default function AdminConfig() { + const { user, isLoading } = useUser(); + const [config, setConfig] = useState>({}); + const [saving, setSaving] = useState(false); + const [saved, setSaved] = useState(false); + + useEffect(() => { + if (user) { + fetch('/api/admin/config').then((r) => r.json()).then(setConfig); + } + }, [user]); + + if (isLoading) return
Loading...
; + if (!user) return ; + + const handleSave = async () => { + setSaving(true); + const res = await fetch('/api/admin/config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(config), + }); + const updated = await res.json(); + setConfig(updated); + setSaving(false); + setSaved(true); + setTimeout(() => setSaved(false), 2000); + }; + + return ( +
+
+

Configuration

+ ← Dashboard +
+ +
+ {Object.entries(config).map(([key, value]) => ( +
+ + setConfig({ ...config, [key]: e.target.value })} + className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm" + /> +
+ ))} +
+ + +
+ ); +} diff --git a/app/admin/leads/page.tsx b/app/admin/leads/page.tsx new file mode 100644 index 0000000..4953f84 --- /dev/null +++ b/app/admin/leads/page.tsx @@ -0,0 +1,78 @@ +'use client'; + +import { useUser } from '@auth0/nextjs-auth0/client'; +import { useEffect, useState } from 'react'; +import Link from 'next/link'; +import { Lead } from '@/lib/types'; + +export default function AdminLeads() { + const { user, isLoading } = useUser(); + const [leads, setLeads] = useState([]); + const [page, setPage] = useState(1); + const [pages, setPages] = useState(1); + const [total, setTotal] = useState(0); + + useEffect(() => { + if (user) { + fetch(`/api/admin/leads?page=${page}`).then((r) => r.json()).then((data) => { + setLeads(data.leads); + setPages(data.pages); + setTotal(data.total); + }); + } + }, [user, page]); + + if (isLoading) return
Loading...
; + if (!user) return ; + + return ( +
+
+

Leads ({total})

+ ← Dashboard +
+ +
+ + + + + + + + + + + {leads.map((lead) => ( + + + + + + + ))} + {leads.length === 0 && ( + + )} + +
EmailOrganisationToolDate
{lead.email}{lead.org_name || '—'}{lead.tool.replace(/-/g, ' ')} + {new Date(lead.created_at).toLocaleDateString()} +
No leads yet
+
+ + {pages > 1 && ( +
+ {Array.from({ length: pages }, (_, i) => ( + + ))} +
+ )} +
+ ); +} diff --git a/app/admin/page.tsx b/app/admin/page.tsx new file mode 100644 index 0000000..17082b0 --- /dev/null +++ b/app/admin/page.tsx @@ -0,0 +1,84 @@ +'use client'; + +import { useUser } from '@auth0/nextjs-auth0/client'; +import { useEffect, useState } from 'react'; +import Link from 'next/link'; + +interface Metrics { + total_sessions: number; + paid_sessions: number; + total_leads: number; + total_tokens: number; + by_tool: { tool: string; count: string }[]; +} + +export default function AdminDashboard() { + const { user, isLoading } = useUser(); + const [metrics, setMetrics] = useState(null); + + useEffect(() => { + if (user) { + fetch('/api/admin/metrics').then((r) => r.json()).then(setMetrics); + } + }, [user]); + + if (isLoading) return
Loading...
; + if (!user) { + return ( +
+

Admin Access Required

+ + Sign In + +
+ ); + } + + return ( +
+
+

Admin Dashboard

+
+ ⚙️ Config + 👥 Leads + Sign Out +
+
+ + {metrics && ( +
+
+
{metrics.total_sessions}
+
Total Sessions
+
+
+
{metrics.paid_sessions}
+
Paid Sessions
+
+
+
{metrics.total_leads}
+
Leads Captured
+
+
+
{metrics.total_tokens.toLocaleString()}
+
Tokens Used
+
+
+ )} + + {metrics?.by_tool && ( +
+

Usage by Tool

+
+ {metrics.by_tool.map((t) => ( +
+ {t.tool.replace(/-/g, ' ')} + {t.count} +
+ ))} +
+
+ )} +
+ ); +} diff --git a/app/api/[tool]/finalize/route.ts b/app/api/[tool]/finalize/route.ts new file mode 100644 index 0000000..5e07e7b --- /dev/null +++ b/app/api/[tool]/finalize/route.ts @@ -0,0 +1,50 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { query } from '@/lib/db'; +import { generate } from '@/lib/ai/client'; +import { prompts } from '@/lib/ai/prompts'; +import { ToolName, Session } from '@/lib/types'; + +export async function POST( + request: NextRequest, + { params }: { params: { tool: string } } +) { + try { + const tool = params.tool as ToolName; + const { session_id, improvements } = await request.json(); + + const [session] = await query( + 'SELECT * FROM sessions WHERE id = $1', + [session_id] + ); + if (!session) { + return NextResponse.json({ error: 'Session not found' }, { status: 404 }); + } + if (!session.is_paid) { + return NextResponse.json({ error: 'Payment required' }, { status: 402 }); + } + if (!session.output) { + return NextResponse.json({ error: 'No output to finalize' }, { status: 400 }); + } + + const toolPrompts = prompts[tool]; + const systemPrompt = 'You are a professional NGO grant writing assistant producing final documents.'; + const userPrompt = toolPrompts.finalize(session.output, improvements || 'No additional improvements.'); + + const result = await generate(systemPrompt, userPrompt); + + await query( + `UPDATE sessions SET output = $1, updated_at = NOW() WHERE id = $2`, + [result.text, session_id] + ); + + await query( + `INSERT INTO usage (session_id, tool, action, tokens_used) VALUES ($1, $2, $3, $4)`, + [session_id, tool, 'finalize', result.tokens] + ); + + return NextResponse.json({ finalized: result.text }); + } catch (error) { + console.error('Finalize error:', error); + return NextResponse.json({ error: 'Failed to finalize' }, { status: 500 }); + } +} diff --git a/app/api/[tool]/generate-draft/route.ts b/app/api/[tool]/generate-draft/route.ts new file mode 100644 index 0000000..565c13c --- /dev/null +++ b/app/api/[tool]/generate-draft/route.ts @@ -0,0 +1,107 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { query, getConfig } from '@/lib/db'; +import { generate } from '@/lib/ai/client'; +import { prompts } from '@/lib/ai/prompts'; +import { ToolName, Session } from '@/lib/types'; + +const validTools: ToolName[] = ['logframe', 'funder-rewriter', 'smart-indicators', 'theory-of-change']; + +export async function POST( + request: NextRequest, + { params }: { params: { tool: string } } +) { + try { + const tool = params.tool as ToolName; + if (!validTools.includes(tool)) { + return NextResponse.json({ error: 'Invalid tool' }, { status: 400 }); + } + + const body = await request.json(); + const { input, email } = body as { input: Record; email?: string }; + + if (!input) { + return NextResponse.json({ error: 'Input required' }, { status: 400 }); + } + + // --- Free tier check --- + // Email is required to use the free tier. If no email, require payment immediately. + let isFreeTier = false; + if (email) { + const freeTierEnabled = (await getConfig('free_tier_enabled')) !== 'false'; + if (freeTierEnabled) { + // Check if this email has ever generated before (any tool) + const existing = await query<{ id: number }>( + 'SELECT id FROM leads WHERE email = $1 LIMIT 1', + [email] + ); + if (existing.length === 0) { + // First time — insert lead, grant free tier + await query( + `INSERT INTO leads (email, org_name, tool) VALUES ($1, $2, $3) + ON CONFLICT (email) DO NOTHING`, + [email, input.organisation || input.project_title || null, tool] + ); + isFreeTier = true; + } + // If email already exists → not free, falls through to paid path below + } + } + + // Create session — track whether it starts as paid (free tier unlock) + const sessions = await query( + `INSERT INTO sessions (tool, email, input, is_paid) VALUES ($1, $2, $3, $4) RETURNING *`, + [tool, email || null, JSON.stringify(input), isFreeTier] + ); + const session = sessions[0]; + + // Generate the draft + const toolPrompts = prompts[tool]; + const systemPrompt = 'You are a professional NGO grant writing assistant. Always respond with well-structured, professional content.'; + const userPrompt = toolPrompts.master(input); + + const result = await generate(systemPrompt, userPrompt); + + // Store full output in session + await query( + `UPDATE sessions SET output = $1, updated_at = NOW() WHERE id = $2`, + [result.text, session.id] + ); + + // Log usage + await query( + `INSERT INTO usage (session_id, tool, action, tokens_used) VALUES ($1, $2, $3, $4)`, + [session.id, tool, 'generate', result.tokens] + ); + + // Preview = first ~40% of content (chars) + const previewEnd = Math.floor(result.text.length * 0.40); + const preview = result.text.substring(0, previewEnd); + + // If free tier or already paid → return full output + if (isFreeTier) { + return NextResponse.json({ + session_id: session.id, + preview, + full_output: result.text, + is_paid: true, + free_tier_used: true, + }); + } + + // Otherwise return preview only — requires payment to unlock + return NextResponse.json({ + session_id: session.id, + preview, + full_length: result.text.length, + is_paid: false, + requires_email: !email, // hint to frontend to prompt for email + }); + + } catch (error) { + console.error('Generate draft error:', error); + return NextResponse.json( + { error: 'Failed to generate draft' }, + { status: 500 } + ); + } +} diff --git a/app/api/[tool]/improve-section/route.ts b/app/api/[tool]/improve-section/route.ts new file mode 100644 index 0000000..0eae5fd --- /dev/null +++ b/app/api/[tool]/improve-section/route.ts @@ -0,0 +1,59 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { query, getConfig } from '@/lib/db'; +import { generate } from '@/lib/ai/client'; +import { prompts } from '@/lib/ai/prompts'; +import { ToolName, Session } from '@/lib/types'; + +export async function POST( + request: NextRequest, + { params }: { params: { tool: string } } +) { + try { + const tool = params.tool as ToolName; + const { session_id, section, feedback } = await request.json(); + + if (!session_id || !section || !feedback) { + return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); + } + + // Check session is paid + const [session] = await query( + 'SELECT * FROM sessions WHERE id = $1', + [session_id] + ); + if (!session) { + return NextResponse.json({ error: 'Session not found' }, { status: 404 }); + } + if (!session.is_paid) { + return NextResponse.json({ error: 'Payment required' }, { status: 402 }); + } + + // Check improvement cap + const maxImprovements = parseInt((await getConfig('max_improvements')) || '5'); + if (session.improvements_used >= maxImprovements) { + return NextResponse.json({ error: 'Improvement limit reached' }, { status: 429 }); + } + + const toolPrompts = prompts[tool]; + const systemPrompt = 'You are a professional NGO grant writing assistant.'; + const userPrompt = toolPrompts.improve(section, feedback); + + const result = await generate(systemPrompt, userPrompt); + + // Update session + await query( + `UPDATE sessions SET improvements_used = improvements_used + 1, updated_at = NOW() WHERE id = $1`, + [session_id] + ); + + await query( + `INSERT INTO usage (session_id, tool, action, tokens_used) VALUES ($1, $2, $3, $4)`, + [session_id, tool, 'improve', result.tokens] + ); + + return NextResponse.json({ improved: result.text }); + } catch (error) { + console.error('Improve section error:', error); + return NextResponse.json({ error: 'Failed to improve section' }, { status: 500 }); + } +} diff --git a/app/api/[tool]/validate/route.ts b/app/api/[tool]/validate/route.ts new file mode 100644 index 0000000..967facd --- /dev/null +++ b/app/api/[tool]/validate/route.ts @@ -0,0 +1,45 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { query } from '@/lib/db'; +import { generate } from '@/lib/ai/client'; +import { prompts } from '@/lib/ai/prompts'; +import { ToolName, Session } from '@/lib/types'; + +export async function POST( + request: NextRequest, + { params }: { params: { tool: string } } +) { + try { + const tool = params.tool as ToolName; + const { session_id } = await request.json(); + + const [session] = await query( + 'SELECT * FROM sessions WHERE id = $1', + [session_id] + ); + if (!session) { + return NextResponse.json({ error: 'Session not found' }, { status: 404 }); + } + if (!session.is_paid) { + return NextResponse.json({ error: 'Payment required' }, { status: 402 }); + } + if (!session.output) { + return NextResponse.json({ error: 'No output to validate' }, { status: 400 }); + } + + const toolPrompts = prompts[tool]; + const systemPrompt = 'You are a senior M&E and grant review specialist.'; + const userPrompt = toolPrompts.validate(session.output); + + const result = await generate(systemPrompt, userPrompt); + + await query( + `INSERT INTO usage (session_id, tool, action, tokens_used) VALUES ($1, $2, $3, $4)`, + [session_id, tool, 'validate', result.tokens] + ); + + return NextResponse.json({ validation: result.text }); + } catch (error) { + console.error('Validate error:', error); + return NextResponse.json({ error: 'Failed to validate' }, { status: 500 }); + } +} diff --git a/app/api/admin/config/route.ts b/app/api/admin/config/route.ts new file mode 100644 index 0000000..314b6ca --- /dev/null +++ b/app/api/admin/config/route.ts @@ -0,0 +1,30 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getAllConfig, setConfig } from '@/lib/db'; +import { requireAdmin } from '@/lib/auth'; + +export async function GET() { + const unauthorized = await requireAdmin(); + if (unauthorized) return unauthorized; + + const config = await getAllConfig(); + return NextResponse.json(config); +} + +export async function POST(request: NextRequest) { + const unauthorized = await requireAdmin(); + if (unauthorized) return unauthorized; + + try { + const updates = await request.json() as Record; + + for (const [key, value] of Object.entries(updates)) { + await setConfig(key, value); + } + + const config = await getAllConfig(); + return NextResponse.json(config); + } catch (error) { + console.error('Config update error:', error); + return NextResponse.json({ error: 'Failed to update config' }, { status: 500 }); + } +} diff --git a/app/api/admin/leads/route.ts b/app/api/admin/leads/route.ts new file mode 100644 index 0000000..04d66d0 --- /dev/null +++ b/app/api/admin/leads/route.ts @@ -0,0 +1,30 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { query } from '@/lib/db'; +import { requireAdmin } from '@/lib/auth'; +import { Lead } from '@/lib/types'; + +export async function GET(request: NextRequest) { + const unauthorized = await requireAdmin(); + if (unauthorized) return unauthorized; + + const { searchParams } = new URL(request.url); + const page = parseInt(searchParams.get('page') || '1'); + const limit = parseInt(searchParams.get('limit') || '50'); + const offset = (page - 1) * limit; + + const leads = await query( + 'SELECT * FROM leads ORDER BY created_at DESC LIMIT $1 OFFSET $2', + [limit, offset] + ); + + const [{ count }] = await query<{ count: string }>( + 'SELECT COUNT(*) as count FROM leads' + ); + + return NextResponse.json({ + leads, + total: parseInt(count), + page, + pages: Math.ceil(parseInt(count) / limit), + }); +} diff --git a/app/api/admin/metrics/route.ts b/app/api/admin/metrics/route.ts new file mode 100644 index 0000000..392fbb0 --- /dev/null +++ b/app/api/admin/metrics/route.ts @@ -0,0 +1,25 @@ +import { NextResponse } from 'next/server'; +import { query } from '@/lib/db'; +import { requireAdmin } from '@/lib/auth'; + +export async function GET() { + const unauthorized = await requireAdmin(); + if (unauthorized) return unauthorized; + + const [sessions] = await query<{ count: string }>('SELECT COUNT(*) as count FROM sessions'); + const [paid] = await query<{ count: string }>('SELECT COUNT(*) as count FROM sessions WHERE is_paid = TRUE'); + const [leads] = await query<{ count: string }>('SELECT COUNT(*) as count FROM leads'); + const [tokens] = await query<{ total: string }>('SELECT COALESCE(SUM(tokens_used), 0) as total FROM usage'); + + const toolBreakdown = await query<{ tool: string; count: string }>( + 'SELECT tool, COUNT(*) as count FROM sessions GROUP BY tool ORDER BY count DESC' + ); + + return NextResponse.json({ + total_sessions: parseInt(sessions.count), + paid_sessions: parseInt(paid.count), + total_leads: parseInt(leads.count), + total_tokens: parseInt(tokens.total), + by_tool: toolBreakdown, + }); +} diff --git a/app/api/auth/[auth0]/route.ts b/app/api/auth/[auth0]/route.ts new file mode 100644 index 0000000..1d2e4e4 --- /dev/null +++ b/app/api/auth/[auth0]/route.ts @@ -0,0 +1,3 @@ +import { handleAuth } from '@auth0/nextjs-auth0'; + +export const GET = handleAuth(); diff --git a/app/api/export/[format]/route.ts b/app/api/export/[format]/route.ts new file mode 100644 index 0000000..2556991 --- /dev/null +++ b/app/api/export/[format]/route.ts @@ -0,0 +1,68 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { query } from '@/lib/db'; +import { Session } from '@/lib/types'; +import { generateDocx } from '@/lib/exports/docx'; +import { generateExcel } from '@/lib/exports/excel'; +import { generatePdf } from '@/lib/exports/pdf'; + +export async function POST( + request: NextRequest, + { params }: { params: { format: string } } +) { + try { + const { session_id } = await request.json(); + const format = params.format; + + const [session] = await query( + 'SELECT * FROM sessions WHERE id = $1', + [session_id] + ); + if (!session) { + return NextResponse.json({ error: 'Session not found' }, { status: 404 }); + } + if (!session.is_paid) { + return NextResponse.json({ error: 'Payment required' }, { status: 402 }); + } + if (!session.output) { + return NextResponse.json({ error: 'No output to export' }, { status: 400 }); + } + + switch (format) { + case 'docx': { + const buffer = await generateDocx(session.output, session.tool); + return new NextResponse(new Uint8Array(buffer), { + headers: { + 'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'Content-Disposition': `attachment; filename="${session.tool}-output.docx"`, + }, + }); + } + case 'excel': { + const buffer = await generateExcel(session.output, session.tool); + return new NextResponse(new Uint8Array(buffer), { + headers: { + 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'Content-Disposition': `attachment; filename="${session.tool}-output.xlsx"`, + }, + }); + } + case 'pdf': { + const buffer = await generatePdf(session.output, session.tool); + return new NextResponse(new Uint8Array(buffer), { + headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment; filename="${session.tool}-output.pdf"`, + }, + }); + } + case 'copy': { + return NextResponse.json({ text: session.output }); + } + default: + return NextResponse.json({ error: 'Invalid format' }, { status: 400 }); + } + } catch (error) { + console.error('Export error:', error); + return NextResponse.json({ error: 'Failed to export' }, { status: 500 }); + } +} diff --git a/app/api/payment/create-checkout/route.ts b/app/api/payment/create-checkout/route.ts new file mode 100644 index 0000000..57bada0 --- /dev/null +++ b/app/api/payment/create-checkout/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { query, getConfig } from '@/lib/db'; +import { createCheckout } from '@/lib/lemonsqueezy'; +import { Session } from '@/lib/types'; + +export async function POST(request: NextRequest) { + try { + const { session_id, email } = await request.json(); + + if (!session_id) { + return NextResponse.json({ error: 'Session ID required' }, { status: 400 }); + } + + // Look up session to get the tool + const sessions = await query( + 'SELECT * FROM sessions WHERE id = $1 LIMIT 1', + [session_id] + ); + const session = sessions[0]; + if (!session) { + return NextResponse.json({ error: 'Session not found' }, { status: 404 }); + } + + // Get per-tool variant ID from admin config + const variantId = await getConfig(`variant_${session.tool}`); + if (!variantId) { + return NextResponse.json( + { error: `Payment not configured for tool: ${session.tool}` }, + { status: 500 } + ); + } + + const checkoutUrl = await createCheckout( + variantId, + session_id, + email || session.email || undefined + ); + + return NextResponse.json({ checkout_url: checkoutUrl }); + } catch (error) { + console.error('Create checkout error:', error); + return NextResponse.json({ error: 'Failed to create checkout' }, { status: 500 }); + } +} diff --git a/app/api/payment/webhook/route.ts b/app/api/payment/webhook/route.ts new file mode 100644 index 0000000..41edf17 --- /dev/null +++ b/app/api/payment/webhook/route.ts @@ -0,0 +1,34 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { query } from '@/lib/db'; +import { verifyWebhookSignature } from '@/lib/lemonsqueezy'; + +export async function POST(request: NextRequest) { + try { + const rawBody = await request.text(); + const signature = request.headers.get('x-signature') || ''; + + if (!verifyWebhookSignature(rawBody, signature)) { + return NextResponse.json({ error: 'Invalid signature' }, { status: 401 }); + } + + const event = JSON.parse(rawBody); + const eventName = event.meta?.event_name; + + if (eventName === 'order_created') { + const sessionId = event.meta?.custom_data?.session_id; + const orderId = event.data?.id; + + if (sessionId) { + await query( + `UPDATE sessions SET is_paid = TRUE, payment_id = $1, updated_at = NOW() WHERE id = $2`, + [String(orderId), sessionId] + ); + } + } + + return NextResponse.json({ received: true }); + } catch (error) { + console.error('Webhook error:', error); + return NextResponse.json({ error: 'Webhook processing failed' }, { status: 500 }); + } +} diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..cc11d90 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,23 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html { + scroll-behavior: smooth; +} + +body { + font-family: 'Inter', system-ui, sans-serif; + background-color: #fafaf8; + color: #111827; +} + +::selection { + background-color: #e8f5ee; + color: #134d2b; +} + +*:focus-visible { + outline: 2px solid #1a6b3c; + outline-offset: 2px; +} diff --git a/app/grant-tools/page.tsx b/app/grant-tools/page.tsx new file mode 100644 index 0000000..41ecb2f --- /dev/null +++ b/app/grant-tools/page.tsx @@ -0,0 +1,136 @@ +const tools = [ + { + title: 'Logframe Generator', + href: '/logframe', + desc: 'Structured goal → outcome → output → activity matrices aligned to donor requirements. Includes OVIs and means of verification.', + inputs: 'Project title, sector, geography, donor, objectives', + sample: 'A 4-level logframe with SMART indicators, assumptions column, and verification sources — ready for annexing.', + }, + { + title: 'SMART Indicators', + href: '/smart-indicators', + desc: 'Evaluation-ready indicators that survive panel review. Auto-aligned to your logframe outputs.', + inputs: 'Logframe or project description, M&E framework preferences', + sample: '8–12 indicators per output with baselines, targets, data sources, and collection frequency.', + }, + { + title: 'Theory of Change', + href: '/theory-of-change', + desc: 'Inputs → activities → outputs → outcomes → impact. Assumptions and evidence gaps surfaced clearly.', + inputs: 'Problem statement, target population, intervention design', + sample: 'A narrative ToC with causal pathways, assumption testing notes, and a visual summary.', + }, + { + title: 'Funder Rewriter', + href: '/funder-rewriter', + desc: 'Reshape existing proposals to match a new funder\'s priorities, tone, and structure — without losing your voice.', + inputs: 'Existing proposal text, target funder name or guidelines', + sample: 'A rewritten proposal section with tracked changes summary and alignment notes.', + }, +]; + +const comparison = [ + { task: 'Build a logframe from scratch', manual: '2–3 days', toolkit: '< 2 minutes' }, + { task: 'Write SMART indicators', manual: '4–6 hours', toolkit: '30 seconds' }, + { task: 'Draft a theory of change', manual: '1–2 days', toolkit: '< 2 minutes' }, + { task: 'Rewrite proposal for new funder', manual: '1 day', toolkit: '45 seconds' }, +]; + +const quotes = [ + { text: 'I used to dread logframe season. Now I finish them over lunch.', author: 'Programme Manager, East Africa regional NGO' }, + { text: 'The SMART indicators it generates are better than what our M&E team was producing manually.', author: 'Grants Lead, UK-based INGO' }, + { text: 'We rewrote a USAID proposal for DFID in under a minute. It took three rounds of review last time.', author: 'Senior Grant Writer, health sector NGO' }, +]; + +export default function GrantToolsPage() { + return ( + <> + {/* Hero */} +
+
+

Grant tools

+

Four tools. Zero fluff.

+

+ Each tool solves one specific grant-writing problem. Describe your project, get a professional draft, refine and export. +

+ + {/* Tool cards */} +
+ {tools.map((t) => ( +
+

{t.title}

+

{t.desc}

+
+
+ Inputs +

{t.inputs}

+
+
+ Sample output +

{t.sample}

+
+
+ + Try it free → + +
+ ))} +
+
+
+ + {/* Comparison */} +
+
+

Manual vs. Toolkit

+
+ + + + + + + + + + {comparison.map((row, i) => ( + + + + + + ))} + +
TaskManualToolkit
{row.task}{row.manual}{row.toolkit}
+
+
+
+ + {/* Practitioner quotes */} +
+
+

From the field

+
+ {quotes.map((q, i) => ( +
+

"{q.text}"

+

{q.author}

+
+ ))} +
+
+
+ + {/* CTA */} +
+
+

Pick a tool. Finish the document.

+

No signup required for your first document.

+ + Try free → + +
+
+ + ); +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..f364ed1 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,44 @@ +import type { Metadata } from 'next'; +import { UserProvider } from '@auth0/nextjs-auth0/client'; +import './globals.css'; + +export const metadata: Metadata = { + title: 'NGO Toolkit Lab — Grant Documents Done.', + description: 'Professional-grade tools for grant writers. Logframes, proposals, SMART indicators, and theories of change — finished before the kettle boils.', +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + +
{children}
+
+ + + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..e249061 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,288 @@ +import Image from 'next/image'; + +export default function Home() { + return ( + <> + {/* ── 1. Hero ── */} +
+
+
+

+ Grant writing tools +

+

+ Finish the logframe.
+ Get your evening back. +

+

+ Professional-grade grant documents — logframes, theories of change, SMART indicators, funder rewrites — finished in minutes, not days. +

+ +

+ Trusted by 120+ organisations across 30 countries. No signup required for your first document. +

+
+
+ Laptop on a clean desk with warm lighting +
+
+
+ + {/* ── 2. Why we built this ── */} +
+
+

Why we built this

+ + {/* Founder note */} +
+

+ I watched talented programme managers burn weekends on logframes — documents that should take hours, not days. + The sector expertise was there. The tooling wasn't. So we built it. +

+

Omair, Founder

+
+ + {/* Practitioner pain cards */} +
+ {[ + { quote: 'I spent three days building a logframe from scratch for a £50k bid. We didn\'t even get shortlisted.', author: 'Programme Manager, East Africa' }, + { quote: 'Every funder wants a different format. I rewrite the same theory of change four times a quarter.', author: 'Grants Lead, UK-based INGO' }, + { quote: 'My indicators passed internal review but got torn apart by the evaluation panel.', author: 'M&E Officer, health sector NGO' }, + ].map((card, i) => ( +
+

"{card.quote}"

+

{card.author}

+
+ ))} +
+ +

+ You know the work. You know the sector. You just need tools that keep up with you. +

+
+
+ + {/* ── 3. Tools ── */} +
+
+

+ What you can build +

+

Four tools. Zero fluff.

+
+ {[ + { + title: 'Logframe Generator', + href: '/logframe', + points: [ + 'Goal, outcomes, outputs, activities — structured to donor spec', + 'Objectively verifiable indicators and means of verification', + 'Export to Word or Excel in one click', + ], + }, + { + title: 'SMART Indicators', + href: '/smart-indicators', + points: [ + 'Specific, measurable, achievable, relevant, time-bound', + 'Aligned to your logframe outputs automatically', + 'Evaluation-ready language that survives panel review', + ], + }, + { + title: 'Theory of Change', + href: '/theory-of-change', + points: [ + 'Inputs → activities → outputs → outcomes → impact', + 'Assumptions and evidence gaps surfaced clearly', + 'Visual and narrative formats for any funder', + ], + }, + { + title: 'Funder Rewriter', + href: '/funder-rewriter', + points: [ + 'Reshape existing proposals to match new funder priorities', + 'Tone, structure, and emphasis adjusted per guidelines', + 'Keep your voice — just sharpen the fit', + ], + }, + ].map((tool) => ( + +

{tool.title}

+
    + {tool.points.map((point, i) => ( +
  • + + {point} +
  • + ))} +
+
+ ))} +
+ Hands typing on a keyboard +
+
+ + {/* ── 4. How It Works ── */} +
+
+ Hands holding coffee beside a laptop +
+

+ How it works +

+

Three steps. One sitting.

+
+ {[ + { + num: '01', + title: 'Describe your project', + desc: 'Fill in the guided form — title, sector, geography, donor requirements. Takes two minutes.', + }, + { + num: '02', + title: 'Get a professional draft', + desc: 'A complete, structured document lands in under thirty seconds. Review it line by line.', + }, + { + num: '03', + title: 'Refine and export', + desc: 'Edit anything inline. When it reads right, export as Word, Excel, or PDF.', + }, + ].map((step) => ( +
+ {step.num} +
+

{step.title}

+

{step.desc}

+
+
+ ))} +
+ + Try the Logframe Generator → + +
+
+
+ + {/* ── 5. Freemium ── */} +
+
+

+ Pricing +

+

Start free. Upgrade when you need to.

+

+ Your first document is completely free — no signup, no credit card. Pay only when you need volume. +

+
+
+

Free

+
    +
  • 1 document per tool
  • +
  • Word and Excel export
  • +
  • No signup required
  • +
+
+
+

Pro — coming soon

+
    +
  • Unlimited documents
  • +
  • PDF export and templates
  • +
  • Save and revisit past work
  • +
  • Priority support
  • +
+
+
+ + Generate your first document free + +
+
+ + {/* ── 6. Emotional Close ── */} +
+ Window at dusk with warm light +
+
+

+ You got into this work to change things — not to spend another weekend on a logframe. +

+

+ Get the document done. Close the laptop. The evening is yours. +

+ + Start now — it's free + +
+
+ + {/* ── 7. Footer ── */} + + + ); +} diff --git a/app/pricing/page.tsx b/app/pricing/page.tsx new file mode 100644 index 0000000..77ab8a4 --- /dev/null +++ b/app/pricing/page.tsx @@ -0,0 +1,143 @@ +'use client'; + +import { useState } from 'react'; + +const faqs = [ + { q: 'Can I use the free tier forever?', a: 'Yes. Your first document per tool is always free — no expiry, no signup.' }, + { q: 'What happens when I hit the free limit?', a: 'You\'ll see a prompt to upgrade. Your existing documents stay accessible.' }, + { q: 'Can I switch between monthly and annual?', a: 'Yes, at any time. Switching to annual applies a prorated credit.' }, + { q: 'Do you offer NGO discounts?', a: 'Pro is already priced for small NGOs. Team pricing includes volume discounts — contact us for 10+ seats.' }, + { q: 'What payment methods do you accept?', a: 'Visa, Mastercard, and bank transfer for annual Team plans.' }, +]; + +const tiers = [ + { + name: 'Free', + monthly: 0, + annual: 0, + desc: 'Try every tool — no signup required.', + features: ['1 document per tool', 'Word & Excel export', 'No account needed'], + cta: 'Start free', + href: '/logframe', + highlight: false, + }, + { + name: 'Pro', + monthly: 29, + annual: 24, + desc: 'For grant writers who submit regularly.', + features: ['Unlimited documents', 'PDF export & templates', 'Save & revisit past work', 'Priority support', 'Funder-specific formatting'], + cta: 'Get Pro', + href: '/logframe', + highlight: true, + }, + { + name: 'Team', + monthly: 79, + annual: 66, + desc: 'For organisations with multiple writers.', + features: ['Everything in Pro', 'Up to 10 seats', 'Shared document library', 'Brand & template presets', 'Dedicated onboarding'], + cta: 'Contact us', + href: 'mailto:hello@ngotoolkitlab.com', + highlight: false, + }, +]; + +export default function PricingPage() { + const [annual, setAnnual] = useState(false); + + return ( + <> +
+
+

Pricing

+

Simple pricing. No surprises.

+

+ Start free, upgrade when you need volume. Cancel any time. +

+ + {/* Toggle */} +
+ Monthly + + + Annual save 17% + +
+ + {/* Tier cards */} +
+ {tiers.map((t) => ( +
+

{t.name}

+

{t.desc}

+
+ + {t.monthly === 0 ? 'Free' : `$${annual ? t.annual : t.monthly}`} + + {t.monthly > 0 && /month} +
+
    + {t.features.map((f, i) => ( +
  • + + {f} +
  • + ))} +
+ + {t.cta} + +
+ ))} +
+
+
+ + {/* FAQ */} +
+
+

Frequently asked questions

+
+ {faqs.map((f, i) => ( +
+

{f.q}

+

{f.a}

+
+ ))} +
+
+
+ + {/* CTA */} +
+
+

Ready to finish the logframe?

+

Your first document is free. No signup needed.

+ + Try free → + +
+
+ + ); +} diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..77e852e --- /dev/null +++ b/bun.lock @@ -0,0 +1,579 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "ngo-toolkit", + "dependencies": { + "@anthropic-ai/sdk": "^0.39.0", + "@auth0/nextjs-auth0": "^3.5.0", + "@react-pdf/renderer": "^4.1.5", + "crypto": "^1.0.1", + "docx": "^9.1.1", + "next": "14.2.21", + "pg": "^8.13.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "xlsx": "^0.18.5", + }, + "devDependencies": { + "@types/crypto-js": "^4.2.2", + "@types/node": "^22.10.2", + "@types/pg": "^8.11.10", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2", + }, + }, + }, + "packages": { + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.39.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg=="], + + "@auth0/nextjs-auth0": ["@auth0/nextjs-auth0@3.8.0", "", { "dependencies": { "@panva/hkdf": "^1.0.2", "cookie": "^0.7.1", "debug": "^4.3.4", "joi": "^17.6.0", "jose": "^4.15.5", "oauth4webapi": "^2.17.0", "openid-client": "^5.7.1", "tslib": "^2.4.0", "url-join": "^4.0.1" }, "peerDependencies": { "next": "^10.0.0 || ^11.0.0 || ^12.3.5 || ^13.5.9 || ^14.2.25 || ^15.2.3" } }, "sha512-xMzpkCuJAZ7tquJOZr7W4Jm9155EDotAACtdd9R9t4ATZgNGUzDQfPMogYJ2O14WB86sZSrp3rpJo4cspQYcSA=="], + + "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], + + "@hapi/hoek": ["@hapi/hoek@9.3.0", "", {}, "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="], + + "@hapi/topo": ["@hapi/topo@5.1.0", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@next/env": ["@next/env@14.2.21", "", {}, "sha512-lXcwcJd5oR01tggjWJ6SrNNYFGuOOMB9c251wUNkjCpkoXOPkDeF/15c3mnVlBqrW4JJXb2kVxDFhC4GduJt2A=="], + + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@14.2.21", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HwEjcKsXtvszXz5q5Z7wCtrHeTTDSTgAbocz45PHMUjU3fBYInfvhR+ZhavDRUYLonm53aHZbB09QtJVJj8T7g=="], + + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@14.2.21", "", { "os": "darwin", "cpu": "x64" }, "sha512-TSAA2ROgNzm4FhKbTbyJOBrsREOMVdDIltZ6aZiKvCi/v0UwFmwigBGeqXDA97TFMpR3LNNpw52CbVelkoQBxA=="], + + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@14.2.21", "", { "os": "linux", "cpu": "arm64" }, "sha512-0Dqjn0pEUz3JG+AImpnMMW/m8hRtl1GQCNbO66V1yp6RswSTiKmnHf3pTX6xMdJYSemf3O4Q9ykiL0jymu0TuA=="], + + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@14.2.21", "", { "os": "linux", "cpu": "arm64" }, "sha512-Ggfw5qnMXldscVntwnjfaQs5GbBbjioV4B4loP+bjqNEb42fzZlAaK+ldL0jm2CTJga9LynBMhekNfV8W4+HBw=="], + + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@14.2.21", "", { "os": "linux", "cpu": "x64" }, "sha512-uokj0lubN1WoSa5KKdThVPRffGyiWlm/vCc/cMkWOQHw69Qt0X1o3b2PyLLx8ANqlefILZh1EdfLRz9gVpG6tg=="], + + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@14.2.21", "", { "os": "linux", "cpu": "x64" }, "sha512-iAEBPzWNbciah4+0yI4s7Pce6BIoxTQ0AGCkxn/UBuzJFkYyJt71MadYQkjPqCQCJAFQ26sYh7MOKdU+VQFgPg=="], + + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@14.2.21", "", { "os": "win32", "cpu": "arm64" }, "sha512-plykgB3vL2hB4Z32W3ktsfqyuyGAPxqwiyrAi2Mr8LlEUhNn9VgkiAl5hODSBpzIfWweX3er1f5uNpGDygfQVQ=="], + + "@next/swc-win32-ia32-msvc": ["@next/swc-win32-ia32-msvc@14.2.21", "", { "os": "win32", "cpu": "ia32" }, "sha512-w5bacz4Vxqrh06BjWgua3Yf7EMDb8iMcVhNrNx8KnJXt8t+Uu0Zg4JHLDL/T7DkTCEEfKXO/Er1fcfWxn2xfPA=="], + + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@14.2.21", "", { "os": "win32", "cpu": "x64" }, "sha512-sT6+llIkzpsexGYZq8cjjthRyRGe5cJVhqh12FmlbxHqna6zsDDK8UNaV7g41T6atFHCJUPeLb3uyAwrBwy0NA=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@panva/hkdf": ["@panva/hkdf@1.2.1", "", {}, "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw=="], + + "@react-pdf/fns": ["@react-pdf/fns@3.1.2", "", {}, "sha512-qTKGUf0iAMGg2+OsUcp9ffKnKi41RukM/zYIWMDJ4hRVYSr89Q7e3wSDW/Koqx3ea3Uy/z3h2y3wPX6Bdfxk6g=="], + + "@react-pdf/font": ["@react-pdf/font@4.0.4", "", { "dependencies": { "@react-pdf/pdfkit": "^4.1.0", "@react-pdf/types": "^2.9.2", "fontkit": "^2.0.2", "is-url": "^1.2.4" } }, "sha512-8YtgGtL511txIEc9AjiilpZ7yjid8uCd8OGUl6jaL3LIHnrToUupSN4IzsMQpVTCMYiDLFnDNQzpZsOYtRS/Pg=="], + + "@react-pdf/image": ["@react-pdf/image@3.0.4", "", { "dependencies": { "@react-pdf/png-js": "^3.0.0", "jay-peg": "^1.1.1" } }, "sha512-z0ogVQE0bKqgXQ5smgzIU857rLV7bMgVdrYsu3UfXDDLSzI7QPvzf6MFTFllX6Dx2rcsF13E01dqKPtJEM799g=="], + + "@react-pdf/layout": ["@react-pdf/layout@4.4.2", "", { "dependencies": { "@react-pdf/fns": "3.1.2", "@react-pdf/image": "^3.0.4", "@react-pdf/primitives": "^4.1.1", "@react-pdf/stylesheet": "^6.1.2", "@react-pdf/textkit": "^6.1.0", "@react-pdf/types": "^2.9.2", "emoji-regex-xs": "^1.0.0", "queue": "^6.0.1", "yoga-layout": "^3.2.1" } }, "sha512-gNu2oh8MiGR+NJZYTJ4c4q0nWCESBI6rKFiodVhE7OeVAjtzZzd6l65wsN7HXdWJqOZD3ttD97iE+tf5SOd/Yg=="], + + "@react-pdf/pdfkit": ["@react-pdf/pdfkit@4.1.0", "", { "dependencies": { "@babel/runtime": "^7.20.13", "@react-pdf/png-js": "^3.0.0", "browserify-zlib": "^0.2.0", "crypto-js": "^4.2.0", "fontkit": "^2.0.2", "jay-peg": "^1.1.1", "linebreak": "^1.1.0", "vite-compatible-readable-stream": "^3.6.1" } }, "sha512-Wm/IOAv0h/U5Ra94c/PltFJGcpTUd/fwVMVeFD6X9tTTPCttIwg0teRG1Lqq617J8K4W7jpL/B0HTH0mjp3QpQ=="], + + "@react-pdf/png-js": ["@react-pdf/png-js@3.0.0", "", { "dependencies": { "browserify-zlib": "^0.2.0" } }, "sha512-eSJnEItZ37WPt6Qv5pncQDxLJRK15eaRwPT+gZoujP548CodenOVp49GST8XJvKMFt9YqIBzGBV/j9AgrOQzVA=="], + + "@react-pdf/primitives": ["@react-pdf/primitives@4.1.1", "", {}, "sha512-IuhxYls1luJb7NUWy6q5avb1XrNaVj9bTNI40U9qGRuS6n7Hje/8H8Qi99Z9UKFV74bBP3DOf3L1wV2qZVgVrQ=="], + + "@react-pdf/reconciler": ["@react-pdf/reconciler@2.0.0", "", { "dependencies": { "object-assign": "^4.1.1", "scheduler": "0.25.0-rc-603e6108-20241029" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-7zaPRujpbHSmCpIrZ+b9HSTJHthcVZzX0Wx7RzvQGsGBUbHP4p6s5itXrAIOuQuPvDepoHGNOvf6xUuMVvdoyw=="], + + "@react-pdf/render": ["@react-pdf/render@4.3.2", "", { "dependencies": { "@babel/runtime": "^7.20.13", "@react-pdf/fns": "3.1.2", "@react-pdf/primitives": "^4.1.1", "@react-pdf/textkit": "^6.1.0", "@react-pdf/types": "^2.9.2", "abs-svg-path": "^0.1.1", "color-string": "^1.9.1", "normalize-svg-path": "^1.1.0", "parse-svg-path": "^0.1.2", "svg-arc-to-cubic-bezier": "^3.2.0" } }, "sha512-el5KYM1sH/PKcO4tRCIm8/AIEmhtraaONbwCrBhFdehoGv6JtgnXiMxHGAvZbI5kEg051GbyP+XIU6f6YbOu6Q=="], + + "@react-pdf/renderer": ["@react-pdf/renderer@4.3.2", "", { "dependencies": { "@babel/runtime": "^7.20.13", "@react-pdf/fns": "3.1.2", "@react-pdf/font": "^4.0.4", "@react-pdf/layout": "^4.4.2", "@react-pdf/pdfkit": "^4.1.0", "@react-pdf/primitives": "^4.1.1", "@react-pdf/reconciler": "^2.0.0", "@react-pdf/render": "^4.3.2", "@react-pdf/types": "^2.9.2", "events": "^3.3.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2", "queue": "^6.0.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-EhPkj35gO9rXIyyx29W3j3axemvVY5RigMmlK4/6Ku0pXB8z9PEE/sz4ZBOShu2uot6V4xiCR3aG+t9IjJJlBQ=="], + + "@react-pdf/stylesheet": ["@react-pdf/stylesheet@6.1.2", "", { "dependencies": { "@react-pdf/fns": "3.1.2", "@react-pdf/types": "^2.9.2", "color-string": "^1.9.1", "hsl-to-hex": "^1.0.0", "media-engine": "^1.0.3", "postcss-value-parser": "^4.1.0" } }, "sha512-E3ftGRYUQGKiN3JOgtGsLDo0hGekA6dmkmi/MYACytmPTKxQRBSO3126MebmCq+t1rgU9uRlREIEawJ+8nzSbw=="], + + "@react-pdf/textkit": ["@react-pdf/textkit@6.1.0", "", { "dependencies": { "@react-pdf/fns": "3.1.2", "bidi-js": "^1.0.2", "hyphen": "^1.6.4", "unicode-properties": "^1.4.1" } }, "sha512-sFlzDC9CDFrJsnL3B/+NHrk9+Advqk7iJZIStiYQDdskbow8GF/AGYrpIk+vWSnh35YxaGbHkqXq53XOxnyrjQ=="], + + "@react-pdf/types": ["@react-pdf/types@2.9.2", "", { "dependencies": { "@react-pdf/font": "^4.0.4", "@react-pdf/primitives": "^4.1.1", "@react-pdf/stylesheet": "^6.1.2" } }, "sha512-dufvpKId9OajLLbgn9q7VLUmyo1Jf+iyGk2ZHmCL8nIDtL8N1Ejh9TH7+pXXrR0tdie1nmnEb5Bz9U7g4hI4/g=="], + + "@sideway/address": ["@sideway/address@4.1.5", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q=="], + + "@sideway/formula": ["@sideway/formula@3.0.1", "", {}, "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="], + + "@sideway/pinpoint": ["@sideway/pinpoint@2.0.0", "", {}, "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="], + + "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], + + "@swc/helpers": ["@swc/helpers@0.5.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A=="], + + "@types/crypto-js": ["@types/crypto-js@4.2.2", "", {}, "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ=="], + + "@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="], + + "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], + + "@types/pg": ["@types/pg@8.18.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q=="], + + "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], + + "@types/react": ["@types/react@18.3.28", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw=="], + + "@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="], + + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + + "abs-svg-path": ["abs-svg-path@0.1.1", "", {}, "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA=="], + + "adler-32": ["adler-32@1.3.1", "", {}, "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A=="], + + "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], + + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "autoprefixer": ["autoprefixer@10.4.27", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001774", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA=="], + + "base64-js": ["base64-js@0.0.8", "", {}, "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.8", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ=="], + + "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "brotli": ["brotli@1.3.3", "", { "dependencies": { "base64-js": "^1.1.2" } }, "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg=="], + + "browserify-zlib": ["browserify-zlib@0.2.0", "", { "dependencies": { "pako": "~1.0.5" } }, "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA=="], + + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + + "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001780", "", {}, "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ=="], + + "cfb": ["cfb@1.2.2", "", { "dependencies": { "adler-32": "~1.3.0", "crc-32": "~1.2.0" } }, "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA=="], + + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + + "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], + + "codepage": ["codepage@1.15.0", "", {}, "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + + "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], + + "crypto": ["crypto@1.0.1", "", {}, "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig=="], + + "crypto-js": ["crypto-js@4.2.0", "", {}, "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "dfa": ["dfa@1.2.0", "", {}, "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="], + + "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], + + "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + + "docx": ["docx@9.6.1", "", { "dependencies": { "@types/node": "^25.2.3", "hash.js": "^1.1.7", "jszip": "^3.10.1", "nanoid": "^5.1.3", "xml": "^1.0.1", "xml-js": "^1.6.8" } }, "sha512-ZJja9/KBUuFC109sCMzovoq2GR2wCG/AuxivjA+OHj/q0TEgJIm3S7yrlUxIy3B+bV8YDj/BiHfWyrRFmyWpDQ=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.313", "", {}, "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA=="], + + "emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "fontkit": ["fontkit@2.0.4", "", { "dependencies": { "@swc/helpers": "^0.5.12", "brotli": "^1.3.2", "clone": "^2.1.2", "dfa": "^1.2.0", "fast-deep-equal": "^3.1.3", "restructure": "^3.0.0", "tiny-inflate": "^1.0.3", "unicode-properties": "^1.4.0", "unicode-trie": "^2.0.0" } }, "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g=="], + + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + + "form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="], + + "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], + + "frac": ["frac@1.1.2", "", {}, "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA=="], + + "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hash.js": ["hash.js@1.1.7", "", { "dependencies": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" } }, "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "hsl-to-hex": ["hsl-to-hex@1.0.0", "", { "dependencies": { "hsl-to-rgb-for-reals": "^1.1.0" } }, "sha512-K6GVpucS5wFf44X0h2bLVRDsycgJmf9FF2elg+CrqD8GcFU8c6vYhgXn8NjUkFCwj+xDFb70qgLbTUm6sxwPmA=="], + + "hsl-to-rgb-for-reals": ["hsl-to-rgb-for-reals@1.1.1", "", {}, "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg=="], + + "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], + + "hyphen": ["hyphen@1.14.1", "", {}, "sha512-kvL8xYl5QMTh+LwohVN72ciOxC0OEV79IPdJSTwEXok9y9QHebXGdFgrED4sWfiax/ODx++CAMk3hMy4XPJPOw=="], + + "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], + + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-url": ["is-url@1.2.4", "", {}, "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww=="], + + "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "jay-peg": ["jay-peg@1.1.1", "", { "dependencies": { "restructure": "^3.0.0" } }, "sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww=="], + + "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + + "joi": ["joi@17.13.3", "", { "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } }, "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA=="], + + "jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="], + + "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "linebreak": ["linebreak@1.1.0", "", { "dependencies": { "base64-js": "0.0.8", "unicode-trie": "^2.0.0" } }, "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-engine": ["media-engine@1.0.3", "", {}, "sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "next": ["next@14.2.21", "", { "dependencies": { "@next/env": "14.2.21", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "graceful-fs": "^4.2.11", "postcss": "8.4.31", "styled-jsx": "5.1.1" }, "optionalDependencies": { "@next/swc-darwin-arm64": "14.2.21", "@next/swc-darwin-x64": "14.2.21", "@next/swc-linux-arm64-gnu": "14.2.21", "@next/swc-linux-arm64-musl": "14.2.21", "@next/swc-linux-x64-gnu": "14.2.21", "@next/swc-linux-x64-musl": "14.2.21", "@next/swc-win32-arm64-msvc": "14.2.21", "@next/swc-win32-ia32-msvc": "14.2.21", "@next/swc-win32-x64-msvc": "14.2.21" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-rZmLwucLHr3/zfDMYbJXbw0ZeoBpirxkXuvsJbk7UPorvPYZhP7vq7aHbKnU7dQNCYIimRrbB2pp3xmf+wsYUg=="], + + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "normalize-svg-path": ["normalize-svg-path@1.1.0", "", { "dependencies": { "svg-arc-to-cubic-bezier": "^3.0.0" } }, "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg=="], + + "oauth4webapi": ["oauth4webapi@2.17.0", "", {}, "sha512-lbC0Z7uzAFNFyzEYRIC+pkSVvDHJTbEW+dYlSBAlCYDe6RxUkJ26bClhk8ocBZip1wfI9uKTe0fm4Ib4RHn6uQ=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + + "oidc-token-hash": ["oidc-token-hash@5.2.0", "", {}, "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw=="], + + "openid-client": ["openid-client@5.7.1", "", { "dependencies": { "jose": "^4.15.9", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew=="], + + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + + "parse-svg-path": ["parse-svg-path@0.1.2", "", {}, "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "pg": ["pg@8.20.0", "", { "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", "pg-protocol": "^1.13.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA=="], + + "pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="], + + "pg-connection-string": ["pg-connection-string@2.12.0", "", {}, "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ=="], + + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], + + "pg-pool": ["pg-pool@3.13.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA=="], + + "pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="], + + "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], + + "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], + + "postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="], + + "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], + + "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], + + "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], + + "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], + + "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], + + "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + + "queue": ["queue@6.0.2", "", { "dependencies": { "inherits": "~2.0.3" } }, "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], + + "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], + + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], + + "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + + "restructure": ["restructure@3.0.2", "", {}, "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], + + "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], + + "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "ssf": ["ssf@0.11.2", "", { "dependencies": { "frac": "~1.1.2" } }, "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g=="], + + "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "styled-jsx": ["styled-jsx@5.1.1", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" } }, "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw=="], + + "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "svg-arc-to-cubic-bezier": ["svg-arc-to-cubic-bezier@3.2.0", "", {}, "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g=="], + + "tailwindcss": ["tailwindcss@3.4.19", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="], + + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + + "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "unicode-properties": ["unicode-properties@1.4.1", "", { "dependencies": { "base64-js": "^1.3.0", "unicode-trie": "^2.0.0" } }, "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg=="], + + "unicode-trie": ["unicode-trie@2.0.0", "", { "dependencies": { "pako": "^0.2.5", "tiny-inflate": "^1.0.0" } }, "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "url-join": ["url-join@4.0.1", "", {}, "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "vite-compatible-readable-stream": ["vite-compatible-readable-stream@3.6.1", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ=="], + + "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "wmf": ["wmf@1.0.2", "", {}, "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw=="], + + "word": ["word@0.3.0", "", {}, "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA=="], + + "xlsx": ["xlsx@0.18.5", "", { "dependencies": { "adler-32": "~1.3.0", "cfb": "~1.2.1", "codepage": "~1.15.0", "crc-32": "~1.2.1", "ssf": "~0.11.2", "wmf": "~1.0.1", "word": "~0.3.0" }, "bin": { "xlsx": "bin/xlsx.njs" } }, "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ=="], + + "xml": ["xml@1.0.1", "", {}, "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw=="], + + "xml-js": ["xml-js@1.6.11", "", { "dependencies": { "sax": "^1.2.4" }, "bin": { "xml-js": "./bin/cli.js" } }, "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g=="], + + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + + "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], + + "@anthropic-ai/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + + "@react-pdf/reconciler/scheduler": ["scheduler@0.25.0-rc-603e6108-20241029", "", {}, "sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA=="], + + "@types/node-fetch/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], + + "brotli/base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "docx/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], + + "docx/nanoid": ["nanoid@5.1.7", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "fontkit/@swc/helpers": ["@swc/helpers@0.5.19", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA=="], + + "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + + "openid-client/object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], + + "readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "unicode-properties/base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "unicode-trie/pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], + + "@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@types/node-fetch/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "docx/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + } +} diff --git a/components/BlurOverlay.tsx b/components/BlurOverlay.tsx new file mode 100644 index 0000000..9797442 --- /dev/null +++ b/components/BlurOverlay.tsx @@ -0,0 +1,28 @@ +'use client'; + +interface BlurOverlayProps { + onUnlock: () => void; + loading?: boolean; +} + +export default function BlurOverlay({ onUnlock, loading }: BlurOverlayProps) { + return ( +
+
+
🔒
+

Unlock Full Output

+

+ Your complete, professional-quality output is ready. Unlock it to access the full document, improvements, validation, and exports. +

+ +

Secure payment via LemonSqueezy

+
+
+ ); +} diff --git a/components/EditableOutput.tsx b/components/EditableOutput.tsx new file mode 100644 index 0000000..28b2f6b --- /dev/null +++ b/components/EditableOutput.tsx @@ -0,0 +1,120 @@ +'use client'; + +import { useState } from 'react'; + +interface EditableOutputProps { + content: string; + sessionId: string; + tool: string; + isPaid: boolean; + improvementsUsed: number; + maxImprovements: number; + onImproved: (newContent: string) => void; +} + +export default function EditableOutput({ + content, + sessionId, + tool, + isPaid, + improvementsUsed, + maxImprovements, + onImproved, +}: EditableOutputProps) { + const [feedback, setFeedback] = useState(''); + const [loading, setLoading] = useState(false); + const [validating, setValidating] = useState(false); + const [validation, setValidation] = useState(null); + + const handleImprove = async () => { + if (!feedback.trim()) return; + setLoading(true); + try { + const res = await fetch(`/api/${tool}/improve-section`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ session_id: sessionId, section: content, feedback }), + }); + const data = await res.json(); + if (data.improved) { + onImproved(data.improved); + setFeedback(''); + } + } catch (e) { + console.error('Improve failed:', e); + } finally { + setLoading(false); + } + }; + + const handleValidate = async () => { + setValidating(true); + try { + const res = await fetch(`/api/${tool}/validate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ session_id: sessionId }), + }); + const data = await res.json(); + if (data.validation) setValidation(data.validation); + } catch (e) { + console.error('Validate failed:', e); + } finally { + setValidating(false); + } + }; + + const remaining = maxImprovements - improvementsUsed; + + return ( +
+
+ {content} +
+ + {isPaid && ( + <> +
+ + + {remaining} improvements remaining + +
+ + {validation && ( +
+

Quality Assessment

+
{validation}
+
+ )} + + {remaining > 0 && ( +
+ setFeedback(e.target.value)} + placeholder="Describe what to improve..." + className="flex-1 border rounded-lg px-3 py-2 text-sm" + onKeyDown={(e) => e.key === 'Enter' && handleImprove()} + /> + +
+ )} + + )} +
+ ); +} diff --git a/components/ExportMenu.tsx b/components/ExportMenu.tsx new file mode 100644 index 0000000..23ec903 --- /dev/null +++ b/components/ExportMenu.tsx @@ -0,0 +1,80 @@ +'use client'; + +import { useState } from 'react'; + +interface ExportMenuProps { + sessionId: string; + disabled?: boolean; +} + +export default function ExportMenu({ sessionId, disabled }: ExportMenuProps) { + const [open, setOpen] = useState(false); + const [exporting, setExporting] = useState(null); + + const handleExport = async (format: string) => { + setExporting(format); + try { + if (format === 'copy') { + const res = await fetch(`/api/export/copy`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ session_id: sessionId }), + }); + const data = await res.json(); + await navigator.clipboard.writeText(data.text); + alert('Copied to clipboard!'); + } else { + const res = await fetch(`/api/export/${format}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ session_id: sessionId }), + }); + const blob = await res.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `output.${format === 'excel' ? 'xlsx' : format}`; + a.click(); + URL.revokeObjectURL(url); + } + } catch (e) { + console.error('Export failed:', e); + } finally { + setExporting(null); + setOpen(false); + } + }; + + const formats = [ + { key: 'copy', label: '📋 Copy to Clipboard', ext: '' }, + { key: 'docx', label: '📄 Word Document', ext: '.docx' }, + { key: 'excel', label: '📊 Excel Spreadsheet', ext: '.xlsx' }, + { key: 'pdf', label: '📕 PDF Document', ext: '.pdf' }, + ]; + + return ( +
+ + {open && ( +
+ {formats.map((f) => ( + + ))} +
+ )} +
+ ); +} diff --git a/components/OutputPanel.tsx b/components/OutputPanel.tsx new file mode 100644 index 0000000..70daa7f --- /dev/null +++ b/components/OutputPanel.tsx @@ -0,0 +1,124 @@ +'use client'; + +import { useState } from 'react'; +import BlurOverlay from './BlurOverlay'; +import EditableOutput from './EditableOutput'; +import ExportMenu from './ExportMenu'; + +type State = 'idle' | 'generating' | 'preview' | 'unlocked' | 'done'; + +interface OutputPanelProps { + tool: string; + sessionId: string | null; + preview: string | null; + fullOutput: string | null; + isPaid: boolean; + onPaymentRequest: () => void; + paymentLoading?: boolean; +} + +export default function OutputPanel({ + tool, + sessionId, + preview, + fullOutput, + isPaid, + onPaymentRequest, + paymentLoading, +}: OutputPanelProps) { + const [output, setOutput] = useState(fullOutput); + const [finalizing, setFinalizing] = useState(false); + + const state: State = !sessionId + ? 'idle' + : !preview + ? 'generating' + : !isPaid + ? 'preview' + : finalizing + ? 'done' + : 'unlocked'; + + const displayOutput = output || fullOutput; + + const handleFinalize = async () => { + setFinalizing(true); + try { + const res = await fetch(`/api/${tool}/finalize`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ session_id: sessionId }), + }); + const data = await res.json(); + if (data.finalized) setOutput(data.finalized); + } catch (e) { + console.error('Finalize failed:', e); + } + }; + + if (state === 'idle') { + return ( +
+
+
📝
+

Fill in the form and click Generate to get started

+
+
+ ); + } + + if (state === 'generating') { + return ( +
+
+
⚙️
+

Generating your output...

+

This may take 15-30 seconds

+
+
+ ); + } + + return ( +
+ {state === 'preview' && ( + <> +
+ {preview} + {'...'.repeat(20)} +
+ + + )} + + {(state === 'unlocked' || state === 'done') && sessionId && displayOutput && ( +
+
+

Your Output

+
+ {!finalizing && ( + + )} + +
+
+ + +
+ )} +
+ ); +} diff --git a/components/ToolForm.tsx b/components/ToolForm.tsx new file mode 100644 index 0000000..fe710f7 --- /dev/null +++ b/components/ToolForm.tsx @@ -0,0 +1,70 @@ +'use client'; + +import { useState } from 'react'; +import { FormField } from '@/lib/types'; + +interface ToolFormProps { + fields: FormField[]; + onSubmit: (data: Record) => void; + loading?: boolean; +} + +export default function ToolForm({ fields, onSubmit, loading }: ToolFormProps) { + const [values, setValues] = useState>({}); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSubmit(values); + }; + + return ( +
+ {fields.map((field) => ( +
+ + {field.type === 'textarea' ? ( +