commit e82ea71c2267ca46622aa518621d790e3a6df09c Author: Mayne0213 Date: Mon Jan 5 02:29:10 2026 +0900 REFACTOR(repo): simplify project structure - Move services/nextjs to nextjs/ - Move deploy/docker/Dockerfile.prod to Dockerfile - Add GitHub Actions workflows (ci.yml, build.yml) - Remove deploy/, services/, scripts/ folders diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4d8d51b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,148 @@ +# Dependencies +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Next.js +.next/ +out/ +build/ + +# Production +dist/ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt + +# Gatsby files +.cache/ +public + +# Storybook build outputs +.out +.storybook-out + +# Temporary folders +tmp/ +temp/ + +# Editor directories and files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Git +.git +.gitignore + +# Docker +Dockerfile* +docker-compose* +.dockerignore + +# Documentation +README.md +*.md + +# Backup files +backups/ +*.backup +*.bak + +# Uploads (should be handled by volumes) +uploads/ + +# SSL certificates +ssl/ + +# Nginx config (handled by volumes) +nginx/ + +# Scripts +scripts/ +backup-scripts/ + +# Prisma +prisma/migrations/ + +# TypeScript +*.tsbuildinfo + +# Testing +coverage/ +.nyc_output/ + +# Misc +.editorconfig +.prettierrc +.eslintrc* + +# Trunk +.trunk \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c7ee892 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,73 @@ +name: Build Docker Image + +on: + push: + branches: [main] + tags: + - 'v*' + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-24.04-arm + permissions: + contents: read + packages: write + + outputs: + image-tag: ${{ steps.meta.outputs.tags }} + image-digest: ${{ steps.build.outputs.digest }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.CR_PAT }} + + - name: Lowercase repository name + id: lowercase + run: | + echo "repo=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT + + - name: Extract metadata (tags, labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.repo }} + tags: | + type=sha,prefix=sha-,format=long + type=raw,value=latest,enable={{is_default_branch}} + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + + - name: Build and push Docker image + id: build + uses: docker/build-push-action@v5 + with: + context: ./nextjs + file: ./Dockerfile + push: true + platforms: linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Display image information + run: | + echo "Image built and pushed successfully!" + echo "Image tags:" + echo "${{ steps.meta.outputs.tags }}" + echo "Digest: ${{ steps.build.outputs.digest }}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..afbf679 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,45 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + lint-and-build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: nextjs/package-lock.json + + - name: Install dependencies + working-directory: nextjs + run: npm ci + + - name: Run ESLint + working-directory: nextjs + run: npm run lint + + - name: Build Next.js application + working-directory: nextjs + run: npm run build + env: + NEXT_TELEMETRY_DISABLED: 1 + + - name: Check build output + working-directory: nextjs + run: | + if [ ! -d ".next" ]; then + echo "Build failed: .next directory not found" + exit 1 + fi + echo "Build completed successfully" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dea05c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,56 @@ +# dependencies +node_modules +services/nextjs/node_modules +.pnp +.pnp.* + +# testing +coverage + +# next.js +services/nextjs/.next/ +.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env* +!.env.example + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# prisma +/prisma/dev.db +/prisma/dev.db-journal +/prisma/*.db +/prisma/*.db-journal + +# IDE +.vscode +.idea +*.swp +*.swo +*~ + +# OS +Thumbs.db + +# trunk +.trunk \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d027e11 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,62 @@ +# Multi-stage build for Next.js application +FROM node:20-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Install dependencies based on the preferred package manager +COPY package.json package-lock.json* ./ +RUN npm ci --only=production + +# Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Generate Prisma Client +RUN npx prisma generate + +# Build the application +ENV NEXT_TELEMETRY_DISABLED=1 +RUN npm run build + +# Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs + +# Copy built application +COPY --from=builder /app/public ./public + +# Set the correct permission for prerender cache +RUN mkdir .next && chown nextjs:nodejs .next + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +# Copy Prisma files +COPY --from=builder /app/prisma ./prisma +COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma + +USER nextjs + +EXPOSE 3000 + +ENV PORT=3000 +ENV HOSTNAME=0.0.0.0 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:3000/api/health || exit 1 + +CMD ["node", "server.js"] diff --git a/nextjs/app/api/auth/login/route.ts b/nextjs/app/api/auth/login/route.ts new file mode 100644 index 0000000..c58945c --- /dev/null +++ b/nextjs/app/api/auth/login/route.ts @@ -0,0 +1,61 @@ +import { NextRequest, NextResponse } from 'next/server' +import { authenticateUser, generateToken } from '@/shared/lib/auth' + +export async function POST(req: NextRequest) { + try { + const { email, password } = await req.json() + + // Validation + if (!email || !password) { + return NextResponse.json( + { error: 'Email and password are required' }, + { status: 400 } + ) + } + + // Authenticate user + const user = await authenticateUser(email, password) + + if (!user) { + return NextResponse.json( + { error: 'Invalid credentials' }, + { status: 401 } + ) + } + + // Generate JWT token + const token = generateToken({ + userId: user.id, + email: user.email, + }) + + // Create response with user data + const response = NextResponse.json({ + message: 'Login successful', + token, + user: { + id: user.id, + email: user.email, + name: user.name, + image: user.image, + }, + }) + + // Set auth token in cookie (HttpOnly for security) + response.cookies.set('auth-token', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60 * 60 * 24 * 7, // 7 days + path: '/', + }) + + return response + } catch (error) { + console.error('Login error:', error) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/nextjs/app/api/auth/logout/route.ts b/nextjs/app/api/auth/logout/route.ts new file mode 100644 index 0000000..289edd6 --- /dev/null +++ b/nextjs/app/api/auth/logout/route.ts @@ -0,0 +1,22 @@ +import { NextRequest, NextResponse } from 'next/server' + +export async function POST(req: NextRequest) { + try { + // Create response + const response = NextResponse.json({ + message: 'Logout successful', + }) + + // Clear auth token cookie + response.cookies.delete('auth-token') + + return response + } catch (error) { + console.error('Logout error:', error) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} + diff --git a/nextjs/app/api/auth/me/route.ts b/nextjs/app/api/auth/me/route.ts new file mode 100644 index 0000000..4bee065 --- /dev/null +++ b/nextjs/app/api/auth/me/route.ts @@ -0,0 +1,54 @@ +import { NextRequest, NextResponse } from 'next/server' +import { verifyToken } from '@/shared/lib/auth' +import { db } from '@/shared/lib/db' + +export async function GET(req: NextRequest) { + try { + // Get token from cookie + const token = req.cookies.get('auth-token')?.value + + if (!token) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + + // Verify token + const payload = verifyToken(token) + + if (!payload) { + return NextResponse.json( + { error: 'Invalid token' }, + { status: 401 } + ) + } + + // Get user from database + const user = await db.user.findUnique({ + where: { id: payload.userId }, + select: { + id: true, + email: true, + name: true, + image: true, + }, + }) + + if (!user) { + return NextResponse.json( + { error: 'User not found' }, + { status: 404 } + ) + } + + return NextResponse.json(user) + } catch (error) { + console.error('Get user error:', error) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} + diff --git a/nextjs/app/api/auth/register/route.ts b/nextjs/app/api/auth/register/route.ts new file mode 100644 index 0000000..2df9109 --- /dev/null +++ b/nextjs/app/api/auth/register/route.ts @@ -0,0 +1,76 @@ +import { NextRequest, NextResponse } from 'next/server' +import { createUser, generateToken } from '@/shared/lib/auth' +import { db } from '@/shared/lib/db' + +export async function POST(req: NextRequest) { + try { + const { email, password, name } = await req.json() + + // Validation + if (!email || !password) { + return NextResponse.json( + { error: 'Email and password are required' }, + { status: 400 } + ) + } + + if (password.length < 6) { + return NextResponse.json( + { error: 'Password must be at least 6 characters' }, + { status: 400 } + ) + } + + // Check if user already exists + const existingUser = await db.user.findUnique({ + where: { email }, + }) + + if (existingUser) { + return NextResponse.json( + { error: 'User already exists' }, + { status: 400 } + ) + } + + // Create user + const user = await createUser(email, password, name) + + // Generate JWT token + const token = generateToken({ + userId: user.id, + email: user.email, + }) + + // Create response with user data + const response = NextResponse.json( + { + message: 'User created successfully', + token, + user: { + id: user.id, + email: user.email, + name: user.name, + } + }, + { status: 201 } + ) + + // Set auth token in cookie (HttpOnly for security) + response.cookies.set('auth-token', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60 * 60 * 24 * 7, // 7 days + path: '/', + }) + + return response + } catch (error) { + console.error('Registration error:', error) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/nextjs/app/api/documents/[id]/public/route.ts b/nextjs/app/api/documents/[id]/public/route.ts new file mode 100644 index 0000000..56bd690 --- /dev/null +++ b/nextjs/app/api/documents/[id]/public/route.ts @@ -0,0 +1,49 @@ +import { NextRequest, NextResponse } from 'next/server' +import { db } from '@/shared/lib/db' + +// GET /api/documents/[id]/public - Get a public document (no auth required) +export async function GET(req: NextRequest) { + try { + const url = new URL(req.url) + const id = url.pathname.split('/')[3] // Extract ID from /api/documents/[id]/public + + if (!id) { + return NextResponse.json( + { error: 'Document ID is required' }, + { status: 400 } + ) + } + + // 공개된 문서만 조회 (인증 없이) + const document = await db.document.findFirst({ + where: { + id, + isPublished: true, // 공개된 문서만 + isArchived: false, + }, + include: { + user: { + select: { + name: true, + email: true, + } + } + } + }) + + if (!document) { + return NextResponse.json( + { error: 'Document not found or not published' }, + { status: 404 } + ) + } + + return NextResponse.json(document) + } catch (error) { + console.error('Error fetching public document:', error) + return NextResponse.json( + { error: 'Failed to fetch document' }, + { status: 500 } + ) + } +} diff --git a/nextjs/app/api/documents/[id]/route.ts b/nextjs/app/api/documents/[id]/route.ts new file mode 100644 index 0000000..bda7963 --- /dev/null +++ b/nextjs/app/api/documents/[id]/route.ts @@ -0,0 +1,202 @@ +import { NextRequest, NextResponse } from 'next/server' +import { withAuth } from '@/shared/lib/middleware' +import { db } from '@/shared/lib/db' +import { deleteFile } from '@/shared/lib/s3' + +// GET /api/documents/[id] - Get a specific document +export const GET = withAuth(async (req: NextRequest, userId: string) => { + try { + const url = new URL(req.url) + const id = url.pathname.split('/').pop() + + if (!id) { + return NextResponse.json( + { error: 'Document ID is required' }, + { status: 400 } + ) + } + + // Allow viewing both archived and non-archived documents + const document = await db.document.findFirst({ + where: { + id, + userId, + }, + }) + + if (!document) { + return NextResponse.json( + { error: 'Document not found' }, + { status: 404 } + ) + } + + return NextResponse.json(document) + } catch (error) { + console.error('Error fetching document:', error) + return NextResponse.json( + { error: 'Failed to fetch document' }, + { status: 500 } + ) + } +}) + +// PUT /api/documents/[id] - Update a specific document +export const PUT = withAuth(async (req: NextRequest, userId: string) => { + try { + const url = new URL(req.url) + const id = url.pathname.split('/').pop() + const { title, content, icon, cover, isPublished } = await req.json() + + if (!id) { + return NextResponse.json( + { error: 'Document ID is required' }, + { status: 400 } + ) + } + + // Check if document exists and belongs to user + const existingDocument = await db.document.findFirst({ + where: { + id, + userId, + isArchived: false, + }, + }) + + if (!existingDocument) { + return NextResponse.json( + { error: 'Document not found' }, + { status: 404 } + ) + } + + const document = await db.document.update({ + where: { id }, + data: { + ...(title && { title }), + ...(content && { content }), + ...(icon !== undefined && { icon }), + ...(cover !== undefined && { cover }), + ...(isPublished !== undefined && { isPublished }), + updatedAt: new Date(), + }, + }) + + return NextResponse.json(document) + } catch (error) { + console.error('Error updating document:', error) + return NextResponse.json( + { error: 'Failed to update document' }, + { status: 500 } + ) + } +}) + +// Helper function to extract image paths from document content +async function extractImagePaths(content: any): Promise { + if (!content) return []; + + const paths: string[] = []; + + try { + const parsed = typeof content === 'string' ? JSON.parse(content) : content; + + // Recursive function to traverse the document structure + const traverse = (node: any) => { + if (!node) return; + + // Check if this is an imageUpload node with a path + if (node.type === 'imageUpload' && node.attrs?.path) { + paths.push(node.attrs.path); + } + + // Recursively check content and children + if (node.content && Array.isArray(node.content)) { + node.content.forEach(traverse); + } + if (node.children && Array.isArray(node.children)) { + node.children.forEach(traverse); + } + }; + + // Check if parsed is an object with content + if (parsed.content) { + traverse(parsed); + } else if (Array.isArray(parsed)) { + parsed.forEach(traverse); + } else if (parsed.type) { + traverse(parsed); + } + } catch (error) { + console.error('Error parsing document content:', error); + } + + return paths; +} + +// DELETE /api/documents/[id] - Archive a specific document +export const DELETE = withAuth(async (req: NextRequest, userId: string) => { + try { + const url = new URL(req.url) + const id = url.pathname.split('/').pop() + + if (!id) { + return NextResponse.json( + { error: 'Document ID is required' }, + { status: 400 } + ) + } + + // Check if document exists and belongs to user + const existingDocument = await db.document.findFirst({ + where: { + id, + userId, + isArchived: false, + }, + }) + + if (!existingDocument) { + return NextResponse.json( + { error: 'Document not found' }, + { status: 404 } + ) + } + + // Extract and delete all image files from the document + const imagePaths = await extractImagePaths(existingDocument.content); + if (imagePaths.length > 0) { + console.log(`Deleting ${imagePaths.length} image files from document ${id}`); + + // Delete all image files + await Promise.allSettled( + imagePaths.map(path => { + try { + return deleteFile(path); + } catch (error) { + console.error(`Failed to delete image file ${path}:`, error); + return Promise.resolve(); + } + }) + ); + } + + // Archive the document instead of deleting it + const document = await db.document.update({ + where: { id }, + data: { + isArchived: true, + updatedAt: new Date(), + }, + }) + + return NextResponse.json({ message: 'Document archived successfully' }) + } catch (error) { + console.error('Error archiving document:', error) + return NextResponse.json( + { error: 'Failed to archive document' }, + { status: 500 } + ) + } +}) diff --git a/nextjs/app/api/documents/route.ts b/nextjs/app/api/documents/route.ts new file mode 100644 index 0000000..eba7cac --- /dev/null +++ b/nextjs/app/api/documents/route.ts @@ -0,0 +1,84 @@ +import { NextRequest, NextResponse } from 'next/server' +import { withAuth } from '@/shared/lib/middleware' +import { db } from '@/shared/lib/db' + +// GET /api/documents - Get all documents for the authenticated user +export const GET = withAuth(async (req: NextRequest, userId: string) => { + try { + const url = new URL(req.url) + const folderId = url.searchParams.get('folderId') + + const documents = await db.document.findMany({ + where: { + userId, + isArchived: false, + ...(folderId && { folderId }), + }, + include: { + folder: { + select: { + id: true, + name: true, + icon: true, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + }) + + return NextResponse.json(documents) + } catch (error) { + console.error('Error fetching documents:', error) + return NextResponse.json( + { error: 'Failed to fetch documents' }, + { status: 500 } + ) + } +}) + +// POST /api/documents - Create a new document +export const POST = withAuth(async (req: NextRequest, userId: string) => { + try { + const { title, parentId, folderId } = await req.json() + + if (!title) { + return NextResponse.json( + { error: 'Title is required' }, + { status: 400 } + ) + } + + const document = await db.document.create({ + data: { + title, + userId, + parentId: parentId || null, + folderId: folderId || null, + content: { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Start writing...', + }, + ], + }, + ], + }, + }, + }) + + return NextResponse.json(document, { status: 201 }) + } catch (error) { + console.error('Error creating document:', error) + return NextResponse.json( + { error: 'Failed to create document' }, + { status: 500 } + ) + } +}) diff --git a/nextjs/app/api/download-url/route.ts b/nextjs/app/api/download-url/route.ts new file mode 100644 index 0000000..867662b --- /dev/null +++ b/nextjs/app/api/download-url/route.ts @@ -0,0 +1,46 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { GetObjectCommand } from '@aws-sdk/client-s3'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import { S3_CONFIG, s3Client } from '@/shared/config/s3'; + +// 파일 다운로드 URL 생성 +const generateDownloadUrl = async (fileKey: string): Promise => { + try { + const command = new GetObjectCommand({ + Bucket: S3_CONFIG.BUCKET_NAME, + Key: fileKey, + }); + + const downloadUrl = await getSignedUrl(s3Client, command, { expiresIn: 3600 }); + + return downloadUrl; + } catch (error) { + throw new Error('다운로드 URL 생성에 실패했습니다.'); + } +}; + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { fileKey } = body; + + if (!fileKey) { + return NextResponse.json( + { error: 'fileKey가 필요합니다.' }, + { status: 400 } + ); + } + + const downloadUrl = await generateDownloadUrl(fileKey); + + return NextResponse.json({ + downloadUrl, + }); + } catch (error: any) { + return NextResponse.json( + { error: error.message || '다운로드 URL 생성에 실패했습니다.' }, + { status: 500 } + ); + } +} + diff --git a/nextjs/app/api/folders/[id]/route.ts b/nextjs/app/api/folders/[id]/route.ts new file mode 100644 index 0000000..1806e47 --- /dev/null +++ b/nextjs/app/api/folders/[id]/route.ts @@ -0,0 +1,264 @@ +import { NextRequest, NextResponse } from 'next/server' +import { withAuth } from '@/shared/lib/middleware' +import { db } from '@/shared/lib/db' + +// GET /api/folders/[id] - Get a specific folder +export const GET = withAuth(async (req: NextRequest, userId: string) => { + try { + const url = new URL(req.url) + const id = url.pathname.split('/').pop() + + if (!id) { + return NextResponse.json( + { error: 'Folder ID is required' }, + { status: 400 } + ) + } + + const folder = await db.folder.findFirst({ + where: { + id, + userId, + isArchived: false, + }, + include: { + documents: { + where: { + isArchived: false, + }, + select: { + id: true, + title: true, + icon: true, + updatedAt: true, + }, + orderBy: { + updatedAt: 'desc', + }, + }, + children: { + where: { + isArchived: false, + }, + orderBy: { + createdAt: 'desc', + }, + }, + parent: { + select: { + id: true, + name: true, + }, + }, + _count: { + select: { + documents: { + where: { + isArchived: false, + }, + }, + children: { + where: { + isArchived: false, + }, + }, + }, + }, + }, + }) + + if (!folder) { + return NextResponse.json( + { error: 'Folder not found' }, + { status: 404 } + ) + } + + return NextResponse.json(folder) + } catch (error) { + console.error('Error fetching folder:', error) + return NextResponse.json( + { error: 'Failed to fetch folder' }, + { status: 500 } + ) + } +}) + +// PUT /api/folders/[id] - Update a specific folder +export const PUT = withAuth(async (req: NextRequest, userId: string) => { + try { + const url = new URL(req.url) + const id = url.pathname.split('/').pop() + const { name, parentId, icon, color } = await req.json() + + if (!id) { + return NextResponse.json( + { error: 'Folder ID is required' }, + { status: 400 } + ) + } + + // Check if folder exists and belongs to user + const existingFolder = await db.folder.findFirst({ + where: { + id, + userId, + isArchived: false, + }, + }) + + if (!existingFolder) { + return NextResponse.json( + { error: 'Folder not found' }, + { status: 404 } + ) + } + + const folder = await db.folder.update({ + where: { id }, + data: { + ...(name && { name }), + ...(parentId !== undefined && { parentId: parentId || null }), + ...(icon !== undefined && { icon }), + ...(color !== undefined && { color }), + updatedAt: new Date(), + }, + include: { + documents: { + where: { + isArchived: false, + }, + select: { + id: true, + title: true, + icon: true, + updatedAt: true, + }, + orderBy: { + updatedAt: 'desc', + }, + }, + children: { + where: { + isArchived: false, + }, + orderBy: { + createdAt: 'desc', + }, + }, + parent: { + select: { + id: true, + name: true, + }, + }, + _count: { + select: { + documents: { + where: { + isArchived: false, + }, + }, + children: { + where: { + isArchived: false, + }, + }, + }, + }, + }, + }) + + return NextResponse.json(folder) + } catch (error) { + console.error('Error updating folder:', error) + return NextResponse.json( + { error: 'Failed to update folder' }, + { status: 500 } + ) + } +}) + +// DELETE /api/folders/[id] - Archive a specific folder +export const DELETE = withAuth(async (req: NextRequest, userId: string) => { + try { + const url = new URL(req.url) + const id = url.pathname.split('/').pop() + + if (!id) { + return NextResponse.json( + { error: 'Folder ID is required' }, + { status: 400 } + ) + } + + // Check if folder exists and belongs to user + const existingFolder = await db.folder.findFirst({ + where: { + id, + userId, + isArchived: false, + }, + }) + + if (!existingFolder) { + return NextResponse.json( + { error: 'Folder not found' }, + { status: 404 } + ) + } + + // Recursively archive the folder and all its contents + const archiveFolderRecursively = async (folderId: string, tx: any) => { + // Archive all documents in this folder + await tx.document.updateMany({ + where: { + folderId: folderId, + isArchived: false, + }, + data: { + isArchived: true, + updatedAt: new Date(), + }, + }) + + // Get all child folders + const childFolders = await tx.folder.findMany({ + where: { + parentId: folderId, + isArchived: false, + }, + select: { + id: true, + }, + }) + + // Recursively archive all child folders + for (const childFolder of childFolders) { + await archiveFolderRecursively(childFolder.id, tx) + } + + // Archive this folder + await tx.folder.update({ + where: { id: folderId }, + data: { + isArchived: true, + updatedAt: new Date(), + }, + }) + } + + // Archive the folder and all its contents + await db.$transaction(async (tx) => { + await archiveFolderRecursively(id, tx) + }) + + return NextResponse.json({ message: 'Folder archived successfully' }) + } catch (error) { + console.error('Error archiving folder:', error) + return NextResponse.json( + { error: 'Failed to archive folder' }, + { status: 500 } + ) + } +}) diff --git a/nextjs/app/api/folders/route.ts b/nextjs/app/api/folders/route.ts new file mode 100644 index 0000000..7680f20 --- /dev/null +++ b/nextjs/app/api/folders/route.ts @@ -0,0 +1,154 @@ +import { NextRequest, NextResponse } from 'next/server' +import { withAuth } from '@/shared/lib/middleware' +import { db } from '@/shared/lib/db' + +// GET /api/folders - Get all folders for the authenticated user +export const GET = withAuth(async (req: NextRequest, userId: string) => { + try { + const folders = await db.folder.findMany({ + where: { + userId, + isArchived: false, + }, + include: { + documents: { + where: { + isArchived: false, + }, + select: { + id: true, + title: true, + icon: true, + updatedAt: true, + }, + }, + children: { + where: { + isArchived: false, + }, + include: { + _count: { + select: { + documents: { + where: { + isArchived: false, + }, + }, + children: { + where: { + isArchived: false, + }, + }, + }, + }, + }, + }, + _count: { + select: { + documents: { + where: { + isArchived: false, + }, + }, + children: { + where: { + isArchived: false, + }, + }, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + }) + + return NextResponse.json(folders) + } catch (error) { + console.error('Error fetching folders:', error) + return NextResponse.json( + { error: 'Failed to fetch folders' }, + { status: 500 } + ) + } +}) + +// POST /api/folders - Create a new folder +export const POST = withAuth(async (req: NextRequest, userId: string) => { + try { + const { name, parentId, icon, color } = await req.json() + + if (!name) { + return NextResponse.json( + { error: 'Folder name is required' }, + { status: 400 } + ) + } + + const folder = await db.folder.create({ + data: { + name, + userId, + parentId: parentId || null, + icon: icon || '📁', + color: color || null, + }, + include: { + documents: { + where: { + isArchived: false, + }, + select: { + id: true, + title: true, + icon: true, + updatedAt: true, + }, + }, + children: { + where: { + isArchived: false, + }, + include: { + _count: { + select: { + documents: { + where: { + isArchived: false, + }, + }, + children: { + where: { + isArchived: false, + }, + }, + }, + }, + }, + }, + _count: { + select: { + documents: { + where: { + isArchived: false, + }, + }, + children: { + where: { + isArchived: false, + }, + }, + }, + }, + }, + }) + + return NextResponse.json(folder, { status: 201 }) + } catch (error) { + console.error('Error creating folder:', error) + return NextResponse.json( + { error: 'Failed to create folder' }, + { status: 500 } + ) + } +}) diff --git a/nextjs/app/api/health/route.ts b/nextjs/app/api/health/route.ts new file mode 100644 index 0000000..5802677 --- /dev/null +++ b/nextjs/app/api/health/route.ts @@ -0,0 +1,27 @@ +import { NextResponse } from 'next/server'; +import { db } from '@/shared/lib/db'; + +export async function GET() { + try { + // Check database connectivity + await db.$queryRaw`SELECT 1`; + + return NextResponse.json({ + status: 'healthy', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + environment: process.env.NODE_ENV, + version: process.env.npm_package_version || '1.0.0', + database: 'connected' + }); + } catch (error) { + return NextResponse.json( + { + status: 'unhealthy', + timestamp: new Date().toISOString(), + error: 'Database connection failed' + }, + { status: 503 } + ); + } +} diff --git a/nextjs/app/api/metadata/route.ts b/nextjs/app/api/metadata/route.ts new file mode 100644 index 0000000..3ce695a --- /dev/null +++ b/nextjs/app/api/metadata/route.ts @@ -0,0 +1,22 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { extractBookmarkMetadata } from '@/shared/lib/metadata-extractor'; + +export async function POST(request: NextRequest) { + try { + const { url } = await request.json(); + + if (!url) { + return NextResponse.json({ error: 'URL is required' }, { status: 400 }); + } + + const metadata = await extractBookmarkMetadata(url); + + return NextResponse.json(metadata); + } catch (error) { + console.error('Error in metadata API:', error); + return NextResponse.json( + { error: 'Failed to fetch metadata' }, + { status: 500 } + ); + } +} diff --git a/nextjs/app/api/templates/route.ts b/nextjs/app/api/templates/route.ts new file mode 100644 index 0000000..91c35ab --- /dev/null +++ b/nextjs/app/api/templates/route.ts @@ -0,0 +1,105 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/shared/lib/prisma'; +import { withAuth } from '@/shared/lib/middleware'; + +async function createTemplate(request: NextRequest, userId: string) { + try { + const { name, description, category, title, content } = await request.json(); + + if (!name || !title || !content) { + return NextResponse.json( + { error: 'Template name, title, and content are required' }, + { status: 400 } + ); + } + + // Verify user exists in database + const user = await prisma.user.findUnique({ + where: { id: userId } + }); + + if (!user) { + return NextResponse.json( + { error: 'User not found' }, + { status: 404 } + ); + } + + // Save template to database (always private to the user) + const template = await prisma.template.create({ + data: { + name, + description: description || '', + category: category || 'General', + title, + content, + isPublic: false, // Always private + userId, + }, + }); + + console.log('Template created:', template); + + return NextResponse.json({ + success: true, + template, + message: 'Template created successfully' + }); + } catch (error) { + console.error('Error creating template:', error); + return NextResponse.json( + { error: 'Failed to create template' }, + { status: 500 } + ); + } +} + +export const POST = withAuth(createTemplate); + +async function getTemplates(request: NextRequest, userId: string) { + try { + // Verify user exists in database + const user = await prisma.user.findUnique({ + where: { id: userId } + }); + + if (!user) { + return NextResponse.json( + { error: 'User not found' }, + { status: 404 } + ); + } + + // Fetch only user's own templates + const templates = await prisma.template.findMany({ + where: { + userId // Only user's own templates + }, + orderBy: { + createdAt: 'desc' + }, + select: { + id: true, + name: true, + description: true, + category: true, + title: true, + content: true, + isPublic: true, + createdAt: true, + updatedAt: true, + userId: true, + } + }); + + return NextResponse.json({ templates }); + } catch (error) { + console.error('Error fetching templates:', error); + return NextResponse.json( + { error: 'Failed to fetch templates' }, + { status: 500 } + ); + } +} + +export const GET = withAuth(getTemplates); diff --git a/nextjs/app/api/upload-url/route.ts b/nextjs/app/api/upload-url/route.ts new file mode 100644 index 0000000..4f77c2f --- /dev/null +++ b/nextjs/app/api/upload-url/route.ts @@ -0,0 +1,57 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { PutObjectCommand } from '@aws-sdk/client-s3'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import { S3_CONFIG, s3Client } from '@/shared/config/s3'; + +// 파일 업로드 URL 생성 +const generateUploadUrl = async ( + fileName: string, + fileType: string, + folder?: string +): Promise<{ uploadUrl: string; fileKey: string }> => { + try { + const fileKey = folder ? `${folder}/${Date.now()}-${fileName}` : `${Date.now()}-${fileName}`; + + const command = new PutObjectCommand({ + Bucket: S3_CONFIG.BUCKET_NAME, + Key: fileKey, + ContentType: fileType, + }); + + const uploadUrl = await getSignedUrl(s3Client, command, { expiresIn: 3600 }); + + return { + uploadUrl, + fileKey, + }; + } catch (error: any) { + throw new Error(`업로드 URL 생성에 실패했습니다: ${error.message}`); + } +}; + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { fileName, fileType, folder } = body; + + if (!fileName || !fileType) { + return NextResponse.json( + { error: 'fileName과 fileType이 필요합니다.' }, + { status: 400 } + ); + } + + const { uploadUrl, fileKey } = await generateUploadUrl(fileName, fileType, folder || 'jotion-uploads'); + + return NextResponse.json({ + uploadUrl, + fileKey, + }); + } catch (error: any) { + return NextResponse.json( + { error: error.message || '업로드 URL 생성에 실패했습니다.' }, + { status: 500 } + ); + } +} + diff --git a/nextjs/app/api/upload/route.ts b/nextjs/app/api/upload/route.ts new file mode 100644 index 0000000..7689ec4 --- /dev/null +++ b/nextjs/app/api/upload/route.ts @@ -0,0 +1,177 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { uploadFileServer, deleteFile } from '@/shared/lib/s3'; +import { cookies } from 'next/headers'; +import { verifyToken } from '@/shared/lib/auth'; + +// File size limits (in bytes) +const MAX_FILE_SIZE = { + image: 10 * 1024 * 1024, // 10MB + audio: 50 * 1024 * 1024, // 50MB + document: 20 * 1024 * 1024, // 20MB +}; + +// Allowed file types +const ALLOWED_TYPES = { + image: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'], + audio: ['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/mp4', 'audio/webm'], + document: [ + 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'text/plain', + 'text/csv', + ], +}; + +export async function POST(request: NextRequest) { + try { + // Verify authentication - check cookie + const cookieStore = await cookies(); + const cookieToken = cookieStore.get('auth-token')?.value; + + const token = cookieToken; + + if (!token) { + console.error('[Upload] No token found in Authorization header or cookie'); + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + const decoded = verifyToken(token); + if (!decoded) { + console.error('[Upload] Token verification failed'); + return NextResponse.json( + { error: 'Invalid token' }, + { status: 401 } + ); + } + console.log('[Upload] Token verified successfully for user:', decoded.userId); + + // Parse form data + const formData = await request.formData(); + const file = formData.get('file') as File; + const fileType = formData.get('type') as 'images' | 'audio' | 'documents'; + + if (!file) { + return NextResponse.json( + { error: 'No file provided' }, + { status: 400 } + ); + } + + // Determine folder based on file type + let folder: 'images' | 'audio' | 'documents' = 'documents'; + let maxSize = MAX_FILE_SIZE.document; + let allowedTypes = ALLOWED_TYPES.document; + + if (fileType === 'images') { + folder = 'images'; + maxSize = MAX_FILE_SIZE.image; + allowedTypes = ALLOWED_TYPES.image; + } else if (fileType === 'audio') { + folder = 'audio'; + maxSize = MAX_FILE_SIZE.audio; + allowedTypes = ALLOWED_TYPES.audio; + } else if (file.type.startsWith('image/')) { + folder = 'images'; + maxSize = MAX_FILE_SIZE.image; + allowedTypes = ALLOWED_TYPES.image; + } else if (file.type.startsWith('audio/')) { + folder = 'audio'; + maxSize = MAX_FILE_SIZE.audio; + allowedTypes = ALLOWED_TYPES.audio; + } + + // Validate file size + if (file.size > maxSize) { + return NextResponse.json( + { error: `File size exceeds maximum limit of ${maxSize / 1024 / 1024}MB` }, + { status: 400 } + ); + } + + // Validate file type + if (!allowedTypes.includes(file.type)) { + return NextResponse.json( + { error: `File type ${file.type} is not allowed` }, + { status: 400 } + ); + } + + // Upload file to S3 + const { url, path } = await uploadFileServer(file, file.name, file.type, folder); + + return NextResponse.json({ + success: true, + url, + path, + fileName: file.name, + fileSize: file.size, + fileType: file.type, + }); + } catch (error) { + console.error('Upload error:', error); + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Failed to upload file' }, + { status: 500 } + ); + } +} + +// DELETE /api/upload - Delete a file from storage +export async function DELETE(request: NextRequest) { + try { + // Verify authentication + const authHeader = request.headers.get('Authorization'); + const cookieStore = await cookies(); + const cookieToken = cookieStore.get('auth-token')?.value; + + const token = authHeader?.startsWith('Bearer ') + ? authHeader.substring(7) + : cookieToken; + + if (!token) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + const decoded = verifyToken(token); + if (!decoded) { + return NextResponse.json( + { error: 'Invalid token' }, + { status: 401 } + ); + } + + const { searchParams } = new URL(request.url); + const filePath = searchParams.get('path'); + + if (!filePath) { + return NextResponse.json( + { error: 'File path is required' }, + { status: 400 } + ); + } + + // Delete file from S3 + await deleteFile(filePath); + + return NextResponse.json({ + success: true, + message: 'File deleted successfully', + }); + } catch (error) { + console.error('Delete file error:', error); + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Failed to delete file' }, + { status: 500 } + ); + } +} + diff --git a/nextjs/app/documents/[id]/page.tsx b/nextjs/app/documents/[id]/page.tsx new file mode 100644 index 0000000..d10422b --- /dev/null +++ b/nextjs/app/documents/[id]/page.tsx @@ -0,0 +1,296 @@ +"use client"; + +import { useAuth } from "@/src/app/providers/auth-provider"; +import { + useDocumentData, + useDocumentSave, + useDocumentActions, + useDocumentTemplates, + useDocumentUtils, + useDocumentHeadings, + useSidebarSearch +} from "@/features/document-edit/model"; +import { Button } from "@/shared/ui/button"; +import { Spinner } from "@/shared/ui/spinner"; +import { RichTextEditor } from "@/widgets/editor/editor/core/rich-text-editor"; +import { DocumentSidebar } from "@/widgets/editor/sidebar/document-sidebar"; +import { ArrowLeft, Save, Clock, User, Eye, BookOpen, FileText, Calendar } from "lucide-react"; +import { useState } from "react"; +import { useRouter, useParams } from "next/navigation"; +import { DocumentDetailSkeleton } from "@/shared/ui/skeleton"; + +const DocumentPage = () => { + const { user } = useAuth(); + const router = useRouter(); + const params = useParams(); + const documentId = params.id as string; + + // 1. 문서 데이터 관리 + const { + document, + isLoading, + title, + setTitle, + content, + setContent, + availableDocuments, + updateDocument, + } = useDocumentData({ documentId }); + + // 2. 저장 기능 + const { + saveDocument, + isSaving, + isAutoSaving, + lastSaved, + } = useDocumentSave({ + documentId, + title, + content, + onSaveSuccess: updateDocument, + }); + + // 3. 문서 액션 + const { + deleteDocument, + shareDocument, + unshareDocument, + isDeleting, + } = useDocumentActions({ + documentId, + onPublishChange: (isPublished) => { + if (document) { + updateDocument({ ...document, isPublished }); + } + }, + }); + + // 4. 템플릿 기능 + const { + createTemplate, + applyTemplate, + } = useDocumentTemplates({ + onApply: (templateContent, templateTitle) => { + setContent(templateContent); + if (templateTitle && !title.trim()) { + setTitle(templateTitle); + } + }, + }); + + // 5. 유틸리티 + const { getWordCount, formatDate } = useDocumentUtils(); + + const [showWordCount, setShowWordCount] = useState(false); + + + if (isLoading) { + return ; + } + + if (!document) { + return ( +
+
+
+ +
+

Document not found

+

+ The document you're looking for doesn't exist or you don't have access to it. +

+ +
+
+ ); + } + + return ( +
+ {/* Modern header */} +
+
+
+ {/* Breadcrumb */} +
+ +
+ {user?.name ? `${user.name}'s` : "My"} Workspace + / + {document.folder && ( + <> + {document.folder.name} + / + + )} + {title || "Untitled"} +
+
+ + {/* Status and actions */} +
+ {/* Auto-save status */} +
+ {isAutoSaving ? ( + <> + + Saving... + + ) : lastSaved ? ( + <> + + Saved {formatDate(lastSaved)} + + ) : null} +
+ + {/* Word count - Hidden on very small screens */} + + + {/* Actions */} +
+ + + {/* Mobile save button */} + +
+
+
+
+
+ + {/* Main content area - Responsive layout */} +
+ {/* Document editor - Full width on mobile/tablet, left side on desktop */} +
+
+ {/* Document header */} +
+ {/* Document icon and title */} +
+
+ {document.icon ? ( + {document.icon} + ) : ( + + )} +
+
+ setTitle(e.target.value)} + className="w-full text-2xl sm:text-3xl lg:text-4xl font-bold text-gray-900 dark:text-white bg-transparent border-none outline-none placeholder-gray-400 dark:placeholder-gray-500" + placeholder="Untitled" + /> +
+
+ + {user?.name || "Anonymous"} +
+
+ + Created {new Date(document.createdAt).toLocaleDateString()} +
+ {document.isPublished && ( + + )} +
+
+
+
+ + {/* Editor */} +
+ +
+
+
+ + {/* Right panel - Document sidebar - Hidden on mobile/tablet, shown on desktop */} +
+ +
+
+ +
+ ); +}; + +export default DocumentPage; diff --git a/nextjs/app/documents/layout.tsx b/nextjs/app/documents/layout.tsx new file mode 100644 index 0000000..5f5a8cb --- /dev/null +++ b/nextjs/app/documents/layout.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { Spinner } from "@/shared/ui/spinner"; +import { useRouter } from "next/navigation"; +import { Navigation } from "@/widgets/documents"; +import { useAuth } from "@/src/app/providers/auth-provider"; +import { useEffect } from "react"; + +const MainLayout = ({ children }: { children: React.ReactNode }) => { + const { user, isLoading } = useAuth(); + const router = useRouter(); + + useEffect(() => { + if (!isLoading && !user) { + router.push("/"); + } + }, [isLoading, user, router]); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (!user) { + return ( +
+ +
+ ); + } + + return ( +
+ +
+ {children} +
+
+ ); +}; + +export default MainLayout; diff --git a/nextjs/app/documents/page.tsx b/nextjs/app/documents/page.tsx new file mode 100644 index 0000000..07dc773 --- /dev/null +++ b/nextjs/app/documents/page.tsx @@ -0,0 +1,202 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useSearchParams } from "next/navigation"; +import { useAuth } from "@/src/app/providers/auth-provider"; +import { useDocumentManagementStore } from "@/features/document-management/model/store"; +import { useFolderManagementStore } from "@/features/folder-management/model/store"; +import { useDocumentStore } from "@/entities/document/model/store"; +import { useFolderStore } from "@/entities/folder/model/store"; +import { Spinner } from "@/shared/ui/spinner"; +import { Header, CreateInput, FolderView, RootView } from "@/widgets/documents"; +import { Button } from "@/shared/ui/button"; +import { Plus, Folder } from "lucide-react"; +import { DocumentsPageSkeleton } from "@/shared/ui/skeleton"; + +const DocumentsPage = () => { + const { user } = useAuth(); + const searchParams = useSearchParams(); + const currentFolderId = searchParams?.get('folder'); + + // Use management stores for operations + const documentManagement = useDocumentManagementStore(); + const folderManagement = useFolderManagementStore(); + + // Use entity stores for state + const documentStore = useDocumentStore(); + const folderStore = useFolderStore(); + + const documents = documentStore.documents; + const folders = folderStore.folders; + const isLoading = documentStore.isLoading || folderStore.isLoading; + + // Destructure specific functions + const { fetchDocuments, createDocument } = documentManagement; + const { fetchFolders, createFolder } = folderManagement; + + const [isCreatingFolder, setIsCreatingFolder] = useState(false); + const [newFolderName, setNewFolderName] = useState(''); + const [refreshKey, setRefreshKey] = useState(0); + + // Fetch initial data + useEffect(() => { + fetchDocuments(); + fetchFolders(); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const handleCreateFolder = async () => { + if (!newFolderName.trim()) return; + + const newFolder = await createFolder(newFolderName, currentFolderId); + if (newFolder) { + setNewFolderName(''); + setIsCreatingFolder(false); + // Refresh data to show the new folder + fetchFolders(); + if (currentFolderId) { + setRefreshKey(prev => prev + 1); + } + } + }; + + const handleCreateDocumentFromHeader = async () => { + await createDocument(currentFolderId); + if (currentFolderId) { + setRefreshKey(prev => prev + 1); + } + }; + + const handleNavigateToFolder = (folderId: string) => { + window.history.pushState({}, '', `/documents?folder=${folderId}`); + window.dispatchEvent(new Event('popstate')); + }; + + const handleGoBack = () => { + window.history.back(); + }; + + const handleFolderDeleted = () => { + // Refresh folders list + fetchFolders(); + fetchDocuments(); + // Force refresh if we're in a folder view + if (currentFolderId) { + setRefreshKey(prev => prev + 1); + } + }; + + // Show loading skeleton while data is being fetched + if (isLoading) { + return ( +
+
setIsCreatingFolder(true)} + onCreateDocument={handleCreateDocumentFromHeader} + /> +
+ +
+
+ ); + } + + return ( +
+ {/* Header */} +
setIsCreatingFolder(true)} + onCreateDocument={handleCreateDocumentFromHeader} + /> + + {/* Create folder input - shown when creating from header */} + {isCreatingFolder && ( +
+ { + setIsCreatingFolder(false); + setNewFolderName(''); + }} + /> +
+ )} + + {/* Main content - Desktop layout */} +
+
+
+
+ {currentFolderId ? ( + + ) : ( + setIsCreatingFolder(true)} + onNavigateToFolder={handleNavigateToFolder} + onFolderDeleted={handleFolderDeleted} + variant="desktop" + /> + )} +
+
+
+
+ + {/* Mobile layout */} +
+
+
+ + {/* Create folder input */} + {isCreatingFolder && ( + { + setIsCreatingFolder(false); + setNewFolderName(''); + }} + /> + )} + + {/* Content */} + {currentFolderId ? ( + + ) : ( + setIsCreatingFolder(true)} + onNavigateToFolder={handleNavigateToFolder} + onFolderDeleted={handleFolderDeleted} + variant="mobile" + /> + )} +
+
+
+
+ ); +}; + +export default DocumentsPage; diff --git a/nextjs/app/documents/todos/page.tsx b/nextjs/app/documents/todos/page.tsx new file mode 100644 index 0000000..4ae3129 --- /dev/null +++ b/nextjs/app/documents/todos/page.tsx @@ -0,0 +1,12 @@ +'use client' + +import { TodoPanel } from '@/widgets/todo' + +export default function TodosPage() { + return ( +
+ +
+ ) +} + diff --git a/nextjs/app/favicon.ico b/nextjs/app/favicon.ico new file mode 100644 index 0000000..fb67988 Binary files /dev/null and b/nextjs/app/favicon.ico differ diff --git a/nextjs/app/globals.css b/nextjs/app/globals.css new file mode 100644 index 0000000..5890da7 --- /dev/null +++ b/nextjs/app/globals.css @@ -0,0 +1,321 @@ +@tailwind base; + @tailwind components; + @tailwind utilities; + + /* Hide scrollbar but keep functionality */ + .scrollbar-hide { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } + .scrollbar-hide::-webkit-scrollbar { + display: none; /* Chrome, Safari and Opera */ + } + + html, + body, + :root { + height: 100%; + } + + @layer base { + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + + --radius: 0.5rem; + } + + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + } + } + + @layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } + } + + /* Tiptap Editor Styles */ + .ProseMirror { + outline: none; + min-height: 200px; + color: #1f2937; /* Default text color for light theme */ + } + + /* Dark theme text color */ + .dark .ProseMirror, + .dark .ProseMirror p, + .dark .ProseMirror div, + .dark .ProseMirror span { + color: #ffffff !important; /* 완전한 하얀색 강제 적용 */ + } + + .ProseMirror p.is-editor-empty:first-child::before { + color: #adb5bd; + content: attr(data-placeholder); + float: left; + height: 0; + pointer-events: none; + } + + /* Dark theme placeholder */ + .dark .ProseMirror p.is-editor-empty:first-child::before { + color: #6b7280; + } + + .ProseMirror h1 { + font-size: 2em; + font-weight: bold; + margin: 0.67em 0; + color: #111827; + } + + .ProseMirror h2 { + font-size: 1.5em; + font-weight: bold; + margin: 0.83em 0; + color: #111827; + } + + .ProseMirror h3 { + font-size: 1.17em; + font-weight: bold; + margin: 1em 0; + color: #111827; + } + + /* Dark theme headings */ + .dark .ProseMirror h1, + .dark .ProseMirror h2, + .dark .ProseMirror h3 { + color: #f9fafb; + } + + .ProseMirror ul, + .ProseMirror ol { + padding-left: 1.5em; + color: inherit; + } + + .ProseMirror li { + color: inherit; + } + + .ProseMirror blockquote { + border-left: 3px solid #e2e8f0; + padding-left: 1rem; + margin: 1rem 0; + font-style: italic; + color: #6b7280; + } + + /* Dark theme blockquote */ + .dark .ProseMirror blockquote { + color: #ffffff; /* 완전한 하얀색 */ + } + + .ProseMirror code { + background-color: #f1f5f9; + border-radius: 0.25rem; + padding: 0.125rem 0.25rem; + font-family: 'Courier New', monospace; + color: #374151; + } + + /* Dark theme inline code */ + .dark .ProseMirror code { + background-color: #374151; + color: #f3f4f6; + } + + .ProseMirror pre { + background-color: #1e293b; + color: #e2e8f0; + border-radius: 0.5rem; + padding: 1rem; + overflow-x: auto; + margin: 1rem 0; + } + + .ProseMirror pre code { + background: none; + padding: 0; + color: inherit; + } + + .ProseMirror table { + border-collapse: collapse; + margin: 1rem 0; + width: 100%; + } + + .ProseMirror th, + .ProseMirror td { + border: 1px solid #e2e8f0; + padding: 0.5rem; + text-align: left; + } + + .ProseMirror th { + background-color: #f8fafc; + font-weight: bold; + } + + .ProseMirror img { + max-width: 100%; + height: auto; + border-radius: 0.5rem; + } + + .ProseMirror a { + color: #3b82f6; + text-decoration: underline; + } + + /* Dark theme links */ + .dark .ProseMirror a { + color: #60a5fa; + } + + .ProseMirror mark { + background-color: #fef08a; + padding: 0.125rem 0.25rem; + border-radius: 0.25rem; + } + + /* Dark theme highlight */ + .dark .ProseMirror mark { + background-color: #fbbf24; + color: #111827; + } + + .ProseMirror ul[data-type="taskList"] { + list-style: none; + padding: 0; + } + + .ProseMirror ul[data-type="taskList"] li { + display: flex; + align-items: flex-start; + } + + .ProseMirror ul[data-type="taskList"] li > label { + flex: 0 0 auto; + margin-right: 0.5rem; + user-select: none; + } + + .ProseMirror ul[data-type="taskList"] li > div { + flex: 1 1 auto; + } + + .ProseMirror ul[data-type="taskList"] input[type="checkbox"] { + cursor: pointer; + } + + /* Dark mode styles */ + .dark .ProseMirror blockquote { + border-left-color: #475569; + } + + .dark .ProseMirror code { + background-color: #334155; + } + + .dark .ProseMirror th { + background-color: #1e293b; + } + + .dark .ProseMirror th, + .dark .ProseMirror td { + border-color: #475569; + } + + /* Line clamp utilities */ + .line-clamp-2 { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } + + .line-clamp-3 { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + } + + /* Code block styles */ + pre { + display: block; + overflow-x: auto; + padding: 1rem; + background: #1e1e1e; + color: #d4d4d4; + border-radius: 0.5rem; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.875rem; + line-height: 1.5; + } + + /* Light mode code block */ + .light pre { + background: #f8f8f8; + color: #333; + } \ No newline at end of file diff --git a/nextjs/app/home/layout.tsx b/nextjs/app/home/layout.tsx new file mode 100644 index 0000000..f09860c --- /dev/null +++ b/nextjs/app/home/layout.tsx @@ -0,0 +1,12 @@ +import { Navbar } from "@/widgets/landing"; + +const MarketingLayout = ({ children }: { children: React.ReactNode }) => { + return ( +
+ +
{children}
+
+ ); +}; + +export default MarketingLayout; \ No newline at end of file diff --git a/nextjs/app/home/page.tsx b/nextjs/app/home/page.tsx new file mode 100644 index 0000000..7d89e43 --- /dev/null +++ b/nextjs/app/home/page.tsx @@ -0,0 +1,17 @@ +import { Heading } from "@/widgets/landing"; +import { Heroes } from "@/widgets/landing"; +import { Footer } from "@/widgets/landing"; + +const MarketingPage = () => { + return ( +
+
+ + +
+
+
+ ); +}; + +export default MarketingPage; diff --git a/nextjs/app/home/share/[id]/page.tsx b/nextjs/app/home/share/[id]/page.tsx new file mode 100644 index 0000000..1799e47 --- /dev/null +++ b/nextjs/app/home/share/[id]/page.tsx @@ -0,0 +1,161 @@ +"use client"; + +import { useState, useEffect } from 'react'; +import { useParams } from 'next/navigation'; +import { DocumentSidebar } from '@/widgets/editor/sidebar/document-sidebar'; +import { RichTextEditor } from '@/widgets/editor/editor/core/rich-text-editor'; +import { Button } from '@/shared/ui/button'; +import { ArrowLeft, Copy, Check, Share2, User, Calendar } from 'lucide-react'; +import Link from 'next/link'; +import type { Document } from '@/shared/types/document'; +import { SharedDocumentSkeleton } from '@/shared/ui/skeleton'; + +interface SharedDocument extends Document { + user: { + name: string; + email: string; + }; +} + +export default function ShareDocumentPage() { + const params = useParams(); + const documentId = params.id as string; + + const [document, setDocument] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchDocument = async () => { + try { + const response = await fetch(`/api/documents/${documentId}/public`); + if (!response.ok) { + throw new Error('Document not found or not published'); + } + + const data = await response.json(); + setDocument(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load document'); + } finally { + setLoading(false); + } + }; + + if (documentId) { + fetchDocument(); + } + }, [documentId]); + + const getWordCount = (content: any): number => { + if (!content) return 0; + + const extractText = (node: any): string => { + if (typeof node === 'string') return node; + if (node.text) return node.text; + if (node.content) { + return node.content.map(extractText).join(' '); + } + return ''; + }; + + const text = extractText(content); + return text.trim().split(/\s+/).filter(word => word.length > 0).length; + }; + + if (loading) { + return ; + } + + if (error || !document) { + return ( +
+
+
📄
+

Document Not Found

+

+ {error || 'This document may not exist or may not be publicly shared.'} +

+ + + +
+
+ ); + } + + const wordCount = getWordCount(document.content); + + return ( +
+ {/* Main Content */} +
+
+ {/* Document Content */} +
+
+
+ +
+
+
+ + {/* Sidebar */} +
+
+ + + {/* Document Info */} +
+

+ + Document Info +

+
+
+ + Author: {document.user.name} +
+
+ + Created: {new Date(document.createdAt).toLocaleDateString()} +
+
+ + Updated: {new Date(document.updatedAt).toLocaleDateString()} +
+
+
+
+
+
+
+ + {/* Footer */} +
+
+
+

This document was shared from Jotion

+

+ Last updated: {new Date(document.updatedAt).toLocaleDateString()} +

+
+
+
+
+ ); +} diff --git a/nextjs/app/home/signIn/page.tsx b/nextjs/app/home/signIn/page.tsx new file mode 100644 index 0000000..7d986ce --- /dev/null +++ b/nextjs/app/home/signIn/page.tsx @@ -0,0 +1,116 @@ +"use client" + +import { useState } from "react" +import { useRouter } from "next/navigation" +import { Button } from "@/shared/ui/button" +import { Logo } from "@/widgets/landing" +import Link from "next/link" +import { useAuth } from "@/src/app/providers/auth-provider" + +export default function LoginPage() { + const [email, setEmail] = useState("") + const [password, setPassword] = useState("") + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState("") + const router = useRouter() + const { login } = useAuth() + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setIsLoading(true) + setError("") + + try { + const response = await fetch("/api/auth/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, password }), + }) + + const data = await response.json() + + if (response.ok) { + // Update auth context with user data + login(data.token, data.user) + + // Redirect to documents page + router.push("/documents") + } else { + setError(data.error || "Login failed") + } + } catch (error) { + setError("An error occurred. Please try again.") + } finally { + setIsLoading(false) + } + } + + return ( +
+
+
+ +

Sign in to your account

+

+ Or{" "} + + create a new account + +

+
+ +
+ {error && ( +
+ {error} +
+ )} + +
+
+ + setEmail(e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-input rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" + placeholder="Enter your email" + /> +
+ +
+ + setPassword(e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-input rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" + placeholder="Enter your password" + /> +
+
+ + +
+
+
+ ) +} diff --git a/nextjs/app/home/signUp/page.tsx b/nextjs/app/home/signUp/page.tsx new file mode 100644 index 0000000..d45f9e5 --- /dev/null +++ b/nextjs/app/home/signUp/page.tsx @@ -0,0 +1,167 @@ +"use client" + +import { useState } from "react" +import { useRouter } from "next/navigation" +import { Button } from "@/shared/ui/button" +import { Logo } from "@/widgets/landing" +import Link from "next/link" +import { useAuth } from "@/src/app/providers/auth-provider" + +export default function SignupPage() { + const [name, setName] = useState("") + const [email, setEmail] = useState("") + const [password, setPassword] = useState("") + const [confirmPassword, setConfirmPassword] = useState("") + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState("") + const router = useRouter() + const { login } = useAuth() + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setIsLoading(true) + setError("") + + if (password !== confirmPassword) { + setError("Passwords do not match") + setIsLoading(false) + return + } + + try { + const response = await fetch("/api/auth/register", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ name, email, password }), + }) + + const data = await response.json() + + if (response.ok) { + // Auto login after successful registration + const loginResponse = await fetch("/api/auth/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, password }), + }) + + const loginData = await loginResponse.json() + + if (loginResponse.ok) { + login(loginData.token, loginData.user) + router.push("/documents") + } else { + router.push("/signIn") + } + } else { + setError(data.error || "Registration failed") + } + } catch (error) { + setError("An error occurred. Please try again.") + } finally { + setIsLoading(false) + } + } + + return ( +
+
+
+ +

Create your account

+

+ Or{" "} + + sign in to your existing account + +

+
+ +
+ {error && ( +
+ {error} +
+ )} + +
+
+ + setName(e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-input rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" + placeholder="Enter your full name" + /> +
+ +
+ + setEmail(e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-input rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" + placeholder="Enter your email" + /> +
+ +
+ + setPassword(e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-input rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" + placeholder="Enter your password" + /> +
+ +
+ + setConfirmPassword(e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-input rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" + placeholder="Confirm your password" + /> +
+
+ + +
+
+
+ ) +} diff --git a/nextjs/app/layout.tsx b/nextjs/app/layout.tsx new file mode 100644 index 0000000..4c47e11 --- /dev/null +++ b/nextjs/app/layout.tsx @@ -0,0 +1,62 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; +import { ThemeProvider } from "@/src/app/providers/theme-provider"; +import { AuthProvider } from "@/src/app/providers/auth-provider"; +import NextTopLoader from 'nextjs-toploader'; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "Jotion", + description: "Generated by Jotion", + icons: { + icon: [ + { + media: "(prefers-color-scheme: light)", + url: "/next.svg", + href: "/next.svg", + }, + { + media: "(prefers-color-scheme: dark)", + url: "/next.svg", + href: "/next.svg", + } + ] + } +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + + + {children} + + + + + ); +} \ No newline at end of file diff --git a/nextjs/components.json b/nextjs/components.json new file mode 100644 index 0000000..1e879bc --- /dev/null +++ b/nextjs/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} \ No newline at end of file diff --git a/nextjs/middleware.ts b/nextjs/middleware.ts new file mode 100644 index 0000000..670183f --- /dev/null +++ b/nextjs/middleware.ts @@ -0,0 +1,71 @@ +import { NextRequest, NextResponse } from 'next/server' +import { verifyToken } from './src/shared/lib/auth' + +export function middleware(request: NextRequest) { + const { pathname } = request.nextUrl + + // Public paths that don't require authentication + const publicPaths = [ + '/', + '/home', + '/home/signIn', + '/home/signUp', + '/home/share', + '/api/auth/login', + '/api/auth/register', + '/api/health', + '/api/metadata', + ] + + // Check if the current path is public + const isPublicPath = publicPaths.some(path => pathname.startsWith(path)) + + // Allow public paths + if (isPublicPath) { + return NextResponse.next() + } + + // Check for authentication on protected paths + if (pathname.startsWith('/documents')) { + // Get token from cookie + const token = request.cookies.get('auth-token')?.value + + if (!token) { + // Redirect to home page if no token + const url = new URL('/home', request.url) + return NextResponse.redirect(url) + } + + // Verify token + const payload = verifyToken(token) + + if (!payload) { + // Invalid token, clear cookie and redirect + const url = new URL('/home', request.url) + const response = NextResponse.redirect(url) + response.cookies.delete('auth-token') + return response + } + + // Token is valid, continue to the requested page + return NextResponse.next() + } + + // Allow all other paths by default + return NextResponse.next() +} + +// Configure which paths this middleware should run on +export const config = { + matcher: [ + /* + * Match all request paths except for the ones starting with: + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + * - public folder + */ + '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', + ], +} + diff --git a/nextjs/next.config.mjs b/nextjs/next.config.mjs new file mode 100644 index 0000000..4e7718c --- /dev/null +++ b/nextjs/next.config.mjs @@ -0,0 +1,48 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + // Enable standalone output for Docker + output: 'standalone', + + // Optimize for Docker + outputFileTracingRoot: '/app', + + // Image optimization + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: '**', + }, + ], + }, + + // API configuration + async headers() { + return [ + { + source: '/api/:path*', + headers: [ + { + key: 'Cache-Control', + value: 'no-store, max-age=0', + }, + ], + }, + ]; + }, + + // Webpack configuration for Docker + webpack: (config, { isServer }) => { + if (!isServer) { + config.resolve.fallback = { + ...config.resolve.fallback, + fs: false, + net: false, + tls: false, + }; + } + return config; + }, +}; + +export default nextConfig; \ No newline at end of file diff --git a/nextjs/package-lock.json b/nextjs/package-lock.json new file mode 100644 index 0000000..8b52692 --- /dev/null +++ b/nextjs/package-lock.json @@ -0,0 +1,6346 @@ +{ + "name": "notion-clone", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "notion-clone", + "version": "0.1.0", + "dependencies": { + "@aws-sdk/client-s3": "^3.918.0", + "@aws-sdk/s3-request-presigner": "^3.918.0", + "@monaco-editor/react": "^4.6.0", + "@prisma/client": "^6.16.3", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-slot": "^1.0.2", + "@supabase/supabase-js": "^2.76.1", + "@tiptap/core": "^3.6.3", + "@tiptap/extension-highlight": "^3.6.3", + "@tiptap/extension-image": "^3.6.3", + "@tiptap/extension-link": "^3.6.3", + "@tiptap/extension-placeholder": "^3.6.3", + "@tiptap/extension-strike": "^3.6.3", + "@tiptap/extension-table": "^3.6.3", + "@tiptap/extension-table-cell": "^3.6.3", + "@tiptap/extension-table-header": "^3.6.3", + "@tiptap/extension-table-row": "^3.6.3", + "@tiptap/extension-task-item": "^3.6.3", + "@tiptap/extension-task-list": "^3.6.3", + "@tiptap/extension-text-align": "^3.6.3", + "@tiptap/extension-underline": "^3.6.3", + "@tiptap/react": "^3.6.3", + "@tiptap/starter-kit": "^3.6.3", + "autoprefixer": "^10.4.21", + "bcryptjs": "^3.0.2", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.0", + "jsonwebtoken": "^9.0.2", + "lucide-react": "^0.544.0", + "monaco-editor": "^0.47.0", + "next": "^15.5.4", + "next-themes": "^0.4.6", + "nextjs-toploader": "^3.9.17", + "node-html-parser": "^7.0.1", + "prisma": "^6.16.3", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "tailwind-merge": "^2.3.0", + "tailwindcss-animate": "^1.0.7", + "usehooks-ts": "^3.1.0", + "zod": "^4.1.12", + "zustand": "^5.0.8" + }, + "devDependencies": { + "@types/bcryptjs": "^3.0.0", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "20.12.11", + "@types/react": "18.3.2", + "@types/react-dom": "^18", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.918.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.918.0.tgz", + "integrity": "sha512-25DhKO0QB4QbhbX1t+txCoRNRvchcq9s3lrDrVJLDwpS7e3cTwSOsicyvMpme6Wk/NSln/lWkYazx8MgUbO6RA==", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/credential-provider-node": "3.918.0", + "@aws-sdk/middleware-bucket-endpoint": "3.914.0", + "@aws-sdk/middleware-expect-continue": "3.917.0", + "@aws-sdk/middleware-flexible-checksums": "3.916.0", + "@aws-sdk/middleware-host-header": "3.914.0", + "@aws-sdk/middleware-location-constraint": "3.914.0", + "@aws-sdk/middleware-logger": "3.914.0", + "@aws-sdk/middleware-recursion-detection": "3.914.0", + "@aws-sdk/middleware-sdk-s3": "3.916.0", + "@aws-sdk/middleware-ssec": "3.914.0", + "@aws-sdk/middleware-user-agent": "3.916.0", + "@aws-sdk/region-config-resolver": "3.914.0", + "@aws-sdk/signature-v4-multi-region": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-endpoints": "3.916.0", + "@aws-sdk/util-user-agent-browser": "3.914.0", + "@aws-sdk/util-user-agent-node": "3.916.0", + "@aws-sdk/xml-builder": "3.914.0", + "@smithy/config-resolver": "^4.4.0", + "@smithy/core": "^3.17.1", + "@smithy/eventstream-serde-browser": "^4.2.3", + "@smithy/eventstream-serde-config-resolver": "^4.3.3", + "@smithy/eventstream-serde-node": "^4.2.3", + "@smithy/fetch-http-handler": "^5.3.4", + "@smithy/hash-blob-browser": "^4.2.4", + "@smithy/hash-node": "^4.2.3", + "@smithy/hash-stream-node": "^4.2.3", + "@smithy/invalid-dependency": "^4.2.3", + "@smithy/md5-js": "^4.2.3", + "@smithy/middleware-content-length": "^4.2.3", + "@smithy/middleware-endpoint": "^4.3.5", + "@smithy/middleware-retry": "^4.4.5", + "@smithy/middleware-serde": "^4.2.3", + "@smithy/middleware-stack": "^4.2.3", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/node-http-handler": "^4.4.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.4", + "@smithy/util-defaults-mode-node": "^4.2.6", + "@smithy/util-endpoints": "^3.2.3", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-retry": "^4.2.3", + "@smithy/util-stream": "^4.5.4", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.3", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.916.0.tgz", + "integrity": "sha512-Eu4PtEUL1MyRvboQnoq5YKg0Z9vAni3ccebykJy615xokVZUdA3di2YxHM/hykDQX7lcUC62q9fVIvh0+UNk/w==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/middleware-host-header": "3.914.0", + "@aws-sdk/middleware-logger": "3.914.0", + "@aws-sdk/middleware-recursion-detection": "3.914.0", + "@aws-sdk/middleware-user-agent": "3.916.0", + "@aws-sdk/region-config-resolver": "3.914.0", + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-endpoints": "3.916.0", + "@aws-sdk/util-user-agent-browser": "3.914.0", + "@aws-sdk/util-user-agent-node": "3.916.0", + "@smithy/config-resolver": "^4.4.0", + "@smithy/core": "^3.17.1", + "@smithy/fetch-http-handler": "^5.3.4", + "@smithy/hash-node": "^4.2.3", + "@smithy/invalid-dependency": "^4.2.3", + "@smithy/middleware-content-length": "^4.2.3", + "@smithy/middleware-endpoint": "^4.3.5", + "@smithy/middleware-retry": "^4.4.5", + "@smithy/middleware-serde": "^4.2.3", + "@smithy/middleware-stack": "^4.2.3", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/node-http-handler": "^4.4.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.4", + "@smithy/util-defaults-mode-node": "^4.2.6", + "@smithy/util-endpoints": "^3.2.3", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-retry": "^4.2.3", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.916.0.tgz", + "integrity": "sha512-1JHE5s6MD5PKGovmx/F1e01hUbds/1y3X8rD+Gvi/gWVfdg5noO7ZCerpRsWgfzgvCMZC9VicopBqNHCKLykZA==", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@aws-sdk/xml-builder": "3.914.0", + "@smithy/core": "^3.17.1", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/signature-v4": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.916.0.tgz", + "integrity": "sha512-3gDeqOXcBRXGHScc6xb7358Lyf64NRG2P08g6Bu5mv1Vbg9PKDyCAZvhKLkG7hkdfAM8Yc6UJNhbFxr1ud/tCQ==", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.916.0.tgz", + "integrity": "sha512-NmooA5Z4/kPFJdsyoJgDxuqXC1C6oPMmreJjbOPqcwo6E/h2jxaG8utlQFgXe5F9FeJsMx668dtxVxSYnAAqHQ==", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/fetch-http-handler": "^5.3.4", + "@smithy/node-http-handler": "^4.4.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/util-stream": "^4.5.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.918.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.918.0.tgz", + "integrity": "sha512-oDViX9z4o8jShY0unX9T7MJqyt+/ojhRB2zoLQVr0Mln7GbXwJ0aUtxgb4PFROG27pJpR11oAaZHzI3LI0jm/A==", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/credential-provider-env": "3.916.0", + "@aws-sdk/credential-provider-http": "3.916.0", + "@aws-sdk/credential-provider-process": "3.916.0", + "@aws-sdk/credential-provider-sso": "3.916.0", + "@aws-sdk/credential-provider-web-identity": "3.918.0", + "@aws-sdk/nested-clients": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/credential-provider-imds": "^4.2.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.918.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.918.0.tgz", + "integrity": "sha512-gl9ECsPB1i8UBPrAJV0HcTn+sgYuD3jYy8ps6KK4c8LznFizwgpah1jd3eF4qq3kPGzrdAE3MKua9OlCCNWAKQ==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.916.0", + "@aws-sdk/credential-provider-http": "3.916.0", + "@aws-sdk/credential-provider-ini": "3.918.0", + "@aws-sdk/credential-provider-process": "3.916.0", + "@aws-sdk/credential-provider-sso": "3.916.0", + "@aws-sdk/credential-provider-web-identity": "3.918.0", + "@aws-sdk/types": "3.914.0", + "@smithy/credential-provider-imds": "^4.2.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.916.0.tgz", + "integrity": "sha512-SXDyDvpJ1+WbotZDLJW1lqP6gYGaXfZJrgFSXIuZjHb75fKeNRgPkQX/wZDdUvCwdrscvxmtyJorp2sVYkMcvA==", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.916.0.tgz", + "integrity": "sha512-gu9D+c+U/Dp1AKBcVxYHNNoZF9uD4wjAKYCjgSN37j4tDsazwMEylbbZLuRNuxfbXtizbo4/TiaxBXDbWM7AkQ==", + "dependencies": { + "@aws-sdk/client-sso": "3.916.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/token-providers": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.918.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.918.0.tgz", + "integrity": "sha512-qQx5qOhSovVF1EEKTc809WsiKzMqEJrlMSOUycDkE+JMgLPIy2pB2LR1crrIeBGgxFUgFsXHvNHbFjRy+AFBdA==", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/nested-clients": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.914.0.tgz", + "integrity": "sha512-mHLsVnPPp4iq3gL2oEBamfpeETFV0qzxRHmcnCfEP3hualV8YF8jbXGmwPCPopUPQDpbYDBHYtXaoClZikCWPQ==", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "@smithy/util-config-provider": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.917.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.917.0.tgz", + "integrity": "sha512-UPBq1ZP2CaxwbncWSbVqkhYXQrmfNiqAtHyBxi413hjRVZ4JhQ1UyH7pz5yqiG8zx2/+Po8cUD4SDUwJgda4nw==", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.916.0.tgz", + "integrity": "sha512-CBRRg6slHHBYAm26AWY/pECHK0vVO/peDoNhZiAzUNt4jV6VftotjszEJ904pKGOr7/86CfZxtCnP3CCs3lQjA==", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-stream": "^4.5.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.914.0.tgz", + "integrity": "sha512-7r9ToySQ15+iIgXMF/h616PcQStByylVkCshmQqcdeynD/lCn2l667ynckxW4+ql0Q+Bo/URljuhJRxVJzydNA==", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.914.0.tgz", + "integrity": "sha512-Mpd0Sm9+GN7TBqGnZg1+dO5QZ/EOYEcDTo7KfvoyrXScMlxvYm9fdrUVMmLdPn/lntweZGV3uNrs+huasGOOTA==", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.914.0.tgz", + "integrity": "sha512-/gaW2VENS5vKvJbcE1umV4Ag3NuiVzpsANxtrqISxT3ovyro29o1RezW/Avz/6oJqjnmgz8soe9J1t65jJdiNg==", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.914.0.tgz", + "integrity": "sha512-yiAjQKs5S2JKYc+GrkvGMwkUvhepXDigEXpSJqUseR/IrqHhvGNuOxDxq+8LbDhM4ajEW81wkiBbU+Jl9G82yQ==", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@aws/lambda-invoke-store": "^0.0.1", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.916.0.tgz", + "integrity": "sha512-pjmzzjkEkpJObzmTthqJPq/P13KoNFuEi/x5PISlzJtHofCNcyXeVAQ90yvY2dQ6UXHf511Rh1/ytiKy2A8M0g==", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/core": "^3.17.1", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/signature-v4": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-stream": "^4.5.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.914.0.tgz", + "integrity": "sha512-V1Oae/oLVbpNb9uWs+v80GKylZCdsbqs2c2Xb1FsAUPtYeSnxFuAWsF3/2AEMSSpFe0dTC5KyWr/eKl2aim9VQ==", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.916.0.tgz", + "integrity": "sha512-mzF5AdrpQXc2SOmAoaQeHpDFsK2GE6EGcEACeNuoESluPI2uYMpuuNMYrUufdnIAIyqgKlis0NVxiahA5jG42w==", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-endpoints": "3.916.0", + "@smithy/core": "^3.17.1", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.916.0.tgz", + "integrity": "sha512-tgg8e8AnVAer0rcgeWucFJ/uNN67TbTiDHfD+zIOPKep0Z61mrHEoeT/X8WxGIOkEn4W6nMpmS4ii8P42rNtnA==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/middleware-host-header": "3.914.0", + "@aws-sdk/middleware-logger": "3.914.0", + "@aws-sdk/middleware-recursion-detection": "3.914.0", + "@aws-sdk/middleware-user-agent": "3.916.0", + "@aws-sdk/region-config-resolver": "3.914.0", + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-endpoints": "3.916.0", + "@aws-sdk/util-user-agent-browser": "3.914.0", + "@aws-sdk/util-user-agent-node": "3.916.0", + "@smithy/config-resolver": "^4.4.0", + "@smithy/core": "^3.17.1", + "@smithy/fetch-http-handler": "^5.3.4", + "@smithy/hash-node": "^4.2.3", + "@smithy/invalid-dependency": "^4.2.3", + "@smithy/middleware-content-length": "^4.2.3", + "@smithy/middleware-endpoint": "^4.3.5", + "@smithy/middleware-retry": "^4.4.5", + "@smithy/middleware-serde": "^4.2.3", + "@smithy/middleware-stack": "^4.2.3", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/node-http-handler": "^4.4.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.4", + "@smithy/util-defaults-mode-node": "^4.2.6", + "@smithy/util-endpoints": "^3.2.3", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-retry": "^4.2.3", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.914.0.tgz", + "integrity": "sha512-KlmHhRbn1qdwXUdsdrJ7S/MAkkC1jLpQ11n+XvxUUUCGAJd1gjC7AjxPZUM7ieQ2zcb8bfEzIU7al+Q3ZT0u7Q==", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/config-resolver": "^4.4.0", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/s3-request-presigner": { + "version": "3.918.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.918.0.tgz", + "integrity": "sha512-s7aBuDNjVr7EcSEHcCkpWCow90BMUlj9LgPXRmO7oAZVnb/7cMl+x9ucAn3lWRttIXqBCIIPK49EaIyZNfjWDg==", + "dependencies": { + "@aws-sdk/signature-v4-multi-region": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-format-url": "3.914.0", + "@smithy/middleware-endpoint": "^4.3.5", + "@smithy/protocol-http": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.916.0.tgz", + "integrity": "sha512-fuzUMo6xU7e0NBzBA6TQ4FUf1gqNbg4woBSvYfxRRsIfKmSMn9/elXXn4sAE5UKvlwVQmYnb6p7dpVRPyFvnQA==", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/protocol-http": "^5.3.3", + "@smithy/signature-v4": "^5.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.916.0.tgz", + "integrity": "sha512-13GGOEgq5etbXulFCmYqhWtpcEQ6WI6U53dvXbheW0guut8fDFJZmEv7tKMTJgiybxh7JHd0rWcL9JQND8DwoQ==", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/nested-clients": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.914.0.tgz", + "integrity": "sha512-kQWPsRDmom4yvAfyG6L1lMmlwnTzm1XwMHOU+G5IFlsP4YEaMtXidDzW/wiivY0QFrhfCz/4TVmu0a2aPU57ug==", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.893.0.tgz", + "integrity": "sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.916.0.tgz", + "integrity": "sha512-bAgUQwvixdsiGNcuZSDAOWbyHlnPtg8G8TyHD6DTfTmKTHUW6tAn+af/ZYJPXEzXhhpwgJqi58vWnsiDhmr7NQ==", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "@smithy/util-endpoints": "^3.2.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.914.0.tgz", + "integrity": "sha512-QpdkoQjvPaYyzZwgk41vFyHQM5s0DsrsbQ8IoPUggQt4HaJUvmL1ShwMcSldbgdzwiRMqXUK8q7jrqUvkYkY6w==", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/querystring-builder": "^4.2.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.893.0.tgz", + "integrity": "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.914.0.tgz", + "integrity": "sha512-rMQUrM1ECH4kmIwlGl9UB0BtbHy6ZuKdWFrIknu8yGTRI/saAucqNTh5EI1vWBxZ0ElhK5+g7zOnUuhSmVQYUA==", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/types": "^4.8.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.916.0.tgz", + "integrity": "sha512-CwfWV2ch6UdjuSV75ZU99N03seEUb31FIUrXBnwa6oONqj/xqXwrxtlUMLx6WH3OJEE4zI3zt5PjlTdGcVwf4g==", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.914.0.tgz", + "integrity": "sha512-k75evsBD5TcIjedycYS7QXQ98AmOtbnxRJOPtCo0IwYRmy7UvqgS/gBL5SmrIqeV6FDSYRQMgdBxSMp6MLmdew==", + "dependencies": { + "@smithy/types": "^4.8.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.0.1.tgz", + "integrity": "sha512-ORHRQ2tmvnBXc8t/X9Z8IcSbBA4xTLKuN873FopzklHMeqBst7YG0d+AX97inkvDX+NChYtSr+qGfcqGFaI8Zw==", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz", + "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz", + "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz", + "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz", + "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz", + "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz", + "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz", + "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz", + "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz", + "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz", + "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz", + "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz", + "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz", + "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz", + "integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz", + "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz", + "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz", + "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.5.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz", + "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz", + "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz", + "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@monaco-editor/loader": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz", + "integrity": "sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@next/env": { + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.4.tgz", + "integrity": "sha512-27SQhYp5QryzIT5uO8hq99C69eLQ7qkzkDPsk3N+GuS2XgOgoYEeOav7Pf8Tn4drECOVDsDg8oj+/DVy8qQL2A==" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.4.tgz", + "integrity": "sha512-nopqz+Ov6uvorej8ndRX6HlxCYWCO3AHLfKK2TYvxoSB2scETOcfm/HSS3piPqc3A+MUgyHoqE6je4wnkjfrOA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.4.tgz", + "integrity": "sha512-QOTCFq8b09ghfjRJKfb68kU9k2K+2wsC4A67psOiMn849K9ZXgCSRQr0oVHfmKnoqCbEmQWG1f2h1T2vtJJ9mA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.4.tgz", + "integrity": "sha512-eRD5zkts6jS3VfE/J0Kt1VxdFqTnMc3QgO5lFE5GKN3KDI/uUpSyK3CjQHmfEkYR4wCOl0R0XrsjpxfWEA++XA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.4.tgz", + "integrity": "sha512-TOK7iTxmXFc45UrtKqWdZ1shfxuL4tnVAOuuJK4S88rX3oyVV4ZkLjtMT85wQkfBrOOvU55aLty+MV8xmcJR8A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.4.tgz", + "integrity": "sha512-7HKolaj+481FSW/5lL0BcTkA4Ueam9SPYWyN/ib/WGAFZf0DGAN8frNpNZYFHtM4ZstrHZS3LY3vrwlIQfsiMA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.4.tgz", + "integrity": "sha512-nlQQ6nfgN0nCO/KuyEUwwOdwQIGjOs4WNMjEUtpIQJPR2NUfmGpW2wkJln1d4nJ7oUzd1g4GivH5GoEPBgfsdw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.4.tgz", + "integrity": "sha512-PcR2bN7FlM32XM6eumklmyWLLbu2vs+D7nJX8OAIoWy69Kef8mfiN4e8TUv2KohprwifdpFKPzIP1njuCjD0YA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.4.tgz", + "integrity": "sha512-1ur2tSHZj8Px/KMAthmuI9FMp/YFusMMGoRNJaRZMOlSkgvLjzosSdQI0cJAKogdHl3qXUQKL9MGaYvKwA7DXg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@prisma/client": { + "version": "6.16.3", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.16.3.tgz", + "integrity": "sha512-JfNfAtXG+/lIopsvoZlZiH2k5yNx87mcTS4t9/S5oufM1nKdXYxOvpDC1XoTCFBa5cQh7uXnbMPsmZrwZY80xw==", + "hasInstallScript": true, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.16.3", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.16.3.tgz", + "integrity": "sha512-VlsLnG4oOuKGGMToEeVaRhoTBZu5H3q51jTQXb/diRags3WV0+BQK5MolJTtP6G7COlzoXmWeS11rNBtvg+qFQ==", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.16.12", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.16.3", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.16.3.tgz", + "integrity": "sha512-89DdqWtdKd7qoc9/qJCKLTazj3W3zPEiz0hc7HfZdpjzm21c7orOUB5oHWJsG+4KbV4cWU5pefq3CuDVYF9vgA==" + }, + "node_modules/@prisma/engines": { + "version": "6.16.3", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.16.3.tgz", + "integrity": "sha512-b+Rl4nzQDcoqe6RIpSHv8f5lLnwdDGvXhHjGDiokObguAAv/O1KaX1Oc69mBW/GFWKQpCkOraobLjU6s1h8HGg==", + "hasInstallScript": true, + "dependencies": { + "@prisma/debug": "6.16.3", + "@prisma/engines-version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a", + "@prisma/fetch-engine": "6.16.3", + "@prisma/get-platform": "6.16.3" + } + }, + "node_modules/@prisma/engines-version": { + "version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a.tgz", + "integrity": "sha512-fftRmosBex48Ph1v2ll1FrPpirwtPZpNkE5CDCY1Lw2SD2ctyrLlVlHiuxDAAlALwWBOkPbAll4+EaqdGuMhJw==" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.16.3", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.16.3.tgz", + "integrity": "sha512-bUoRIkVaI+CCaVGrSfcKev0/Mk4ateubqWqGZvQ9uCqFv2ENwWIR3OeNuGin96nZn5+SkebcD7RGgKr/+mJelw==", + "dependencies": { + "@prisma/debug": "6.16.3", + "@prisma/engines-version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a", + "@prisma/get-platform": "6.16.3" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.16.3", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.16.3.tgz", + "integrity": "sha512-X1LxiFXinJ4iQehrodGp0f66Dv6cDL0GbRlcCoLtSu6f4Wi+hgo7eND/afIs5029GQLgNWKZ46vn8hjyXTsHLA==", + "dependencies": { + "@prisma/debug": "6.16.3" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==" + }, + "node_modules/@remirror/core-constants": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", + "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==" + }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.3.tgz", + "integrity": "sha512-xWL9Mf8b7tIFuAlpjKtRPnHrR8XVrwTj5NPYO/QwZPtc0SDLsPxb56V5tzi5yspSMytISHybifez+4jlrx0vkQ==", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.0.tgz", + "integrity": "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.1.tgz", + "integrity": "sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ==", + "dependencies": { + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.0.tgz", + "integrity": "sha512-Kkmz3Mup2PGp/HNJxhCWkLNdlajJORLSjwkcfrj0E7nu6STAEdcMR1ir5P9/xOmncx8xXfru0fbUYLlZog/cFg==", + "dependencies": { + "@smithy/node-config-provider": "^4.3.3", + "@smithy/types": "^4.8.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-endpoints": "^3.2.3", + "@smithy/util-middleware": "^4.2.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.17.1.tgz", + "integrity": "sha512-V4Qc2CIb5McABYfaGiIYLTmo/vwNIK7WXI5aGveBd9UcdhbOMwcvIMxIw/DJj1S9QgOMa/7FBkarMdIC0EOTEQ==", + "dependencies": { + "@smithy/middleware-serde": "^4.2.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-stream": "^4.5.4", + "@smithy/util-utf8": "^4.2.0", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.3.tgz", + "integrity": "sha512-hA1MQ/WAHly4SYltJKitEsIDVsNmXcQfYBRv2e+q04fnqtAX5qXaybxy/fhUeAMCnQIdAjaGDb04fMHQefWRhw==", + "dependencies": { + "@smithy/node-config-provider": "^4.3.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.3.tgz", + "integrity": "sha512-rcr0VH0uNoMrtgKuY7sMfyKqbHc4GQaQ6Yp4vwgm+Z6psPuOgL+i/Eo/QWdXRmMinL3EgFM0Z1vkfyPyfzLmjw==", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.8.0", + "@smithy/util-hex-encoding": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.3.tgz", + "integrity": "sha512-EcS0kydOr2qJ3vV45y7nWnTlrPmVIMbUFOZbMG80+e2+xePQISX9DrcbRpVRFTS5Nqz3FiEbDcTCAV0or7bqdw==", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.3.tgz", + "integrity": "sha512-GewKGZ6lIJ9APjHFqR2cUW+Efp98xLu1KmN0jOWxQ1TN/gx3HTUPVbLciFD8CfScBj2IiKifqh9vYFRRXrYqXA==", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.3.tgz", + "integrity": "sha512-uQobOTQq2FapuSOlmGLUeGTpvcBLE5Fc7XjERUSk4dxEi4AhTwuyHYZNAvL4EMUp7lzxxkKDFaJ1GY0ovrj0Kg==", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.3.tgz", + "integrity": "sha512-QIvH/CKOk1BZPz/iwfgbh1SQD5Y0lpaw2kLA8zpLRRtYMPXeYUEWh+moTaJyqDaKlbrB174kB7FSRFiZ735tWw==", + "dependencies": { + "@smithy/eventstream-codec": "^4.2.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.4.tgz", + "integrity": "sha512-bwigPylvivpRLCm+YK9I5wRIYjFESSVwl8JQ1vVx/XhCw0PtCi558NwTnT2DaVCl5pYlImGuQTSwMsZ+pIavRw==", + "dependencies": { + "@smithy/protocol-http": "^5.3.3", + "@smithy/querystring-builder": "^4.2.3", + "@smithy/types": "^4.8.0", + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.4.tgz", + "integrity": "sha512-W7eIxD+rTNsLB/2ynjmbdeP7TgxRXprfvqQxKFEfy9HW2HeD7t+g+KCIrY0pIn/GFjA6/fIpH+JQnfg5TTk76Q==", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.2.0", + "@smithy/chunked-blob-reader-native": "^4.2.1", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.3.tgz", + "integrity": "sha512-6+NOdZDbfuU6s1ISp3UOk5Rg953RJ2aBLNLLBEcamLjHAg1Po9Ha7QIB5ZWhdRUVuOUrT8BVFR+O2KIPmw027g==", + "dependencies": { + "@smithy/types": "^4.8.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.3.tgz", + "integrity": "sha512-EXMSa2yiStVII3x/+BIynyOAZlS7dGvI7RFrzXa/XssBgck/7TXJIvnjnCu328GY/VwHDC4VeDyP1S4rqwpYag==", + "dependencies": { + "@smithy/types": "^4.8.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.3.tgz", + "integrity": "sha512-Cc9W5DwDuebXEDMpOpl4iERo8I0KFjTnomK2RMdhhR87GwrSmUmwMxS4P5JdRf+LsjOdIqumcerwRgYMr/tZ9Q==", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.3.tgz", + "integrity": "sha512-5+4bUEJQi/NRgzdA5SVXvAwyvEnD0ZAiKzV3yLO6dN5BG8ScKBweZ8mxXXUtdxq+Dx5k6EshKk0XJ7vgvIPSnA==", + "dependencies": { + "@smithy/types": "^4.8.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.3.tgz", + "integrity": "sha512-/atXLsT88GwKtfp5Jr0Ks1CSa4+lB+IgRnkNrrYP0h1wL4swHNb0YONEvTceNKNdZGJsye+W2HH8W7olbcPUeA==", + "dependencies": { + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.5.tgz", + "integrity": "sha512-SIzKVTvEudFWJbxAaq7f2GvP3jh2FHDpIFI6/VAf4FOWGFZy0vnYMPSRj8PGYI8Hjt29mvmwSRgKuO3bK4ixDw==", + "dependencies": { + "@smithy/core": "^3.17.1", + "@smithy/middleware-serde": "^4.2.3", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "@smithy/util-middleware": "^4.2.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.5.tgz", + "integrity": "sha512-DCaXbQqcZ4tONMvvdz+zccDE21sLcbwWoNqzPLFlZaxt1lDtOE2tlVpRSwcTOJrjJSUThdgEYn7HrX5oLGlK9A==", + "dependencies": { + "@smithy/node-config-provider": "^4.3.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/service-error-classification": "^4.2.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-retry": "^4.2.3", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.3.tgz", + "integrity": "sha512-8g4NuUINpYccxiCXM5s1/V+uLtts8NcX4+sPEbvYQDZk4XoJfDpq5y2FQxfmUL89syoldpzNzA0R9nhzdtdKnQ==", + "dependencies": { + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.3.tgz", + "integrity": "sha512-iGuOJkH71faPNgOj/gWuEGS6xvQashpLwWB1HjHq1lNNiVfbiJLpZVbhddPuDbx9l4Cgl0vPLq5ltRfSaHfspA==", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.3.tgz", + "integrity": "sha512-NzI1eBpBSViOav8NVy1fqOlSfkLgkUjUTlohUSgAEhHaFWA3XJiLditvavIP7OpvTjDp5u2LhtlBhkBlEisMwA==", + "dependencies": { + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.3.tgz", + "integrity": "sha512-MAwltrDB0lZB/H6/2M5PIsISSwdI5yIh6DaBB9r0Flo9nx3y0dzl/qTMJPd7tJvPdsx6Ks/cwVzheGNYzXyNbQ==", + "dependencies": { + "@smithy/abort-controller": "^4.2.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/querystring-builder": "^4.2.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.3.tgz", + "integrity": "sha512-+1EZ+Y+njiefCohjlhyOcy1UNYjT+1PwGFHCxA/gYctjg3DQWAU19WigOXAco/Ql8hZokNehpzLd0/+3uCreqQ==", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.3.tgz", + "integrity": "sha512-Mn7f/1aN2/jecywDcRDvWWWJF4uwg/A0XjFMJtj72DsgHTByfjRltSqcT9NyE9RTdBSN6X1RSXrhn/YWQl8xlw==", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.3.tgz", + "integrity": "sha512-LOVCGCmwMahYUM/P0YnU/AlDQFjcu+gWbFJooC417QRB/lDJlWSn8qmPSDp+s4YVAHOgtgbNG4sR+SxF/VOcJQ==", + "dependencies": { + "@smithy/types": "^4.8.0", + "@smithy/util-uri-escape": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.3.tgz", + "integrity": "sha512-cYlSNHcTAX/wc1rpblli3aUlLMGgKZ/Oqn8hhjFASXMCXjIqeuQBei0cnq2JR8t4RtU9FpG6uyl6PxyArTiwKA==", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.3.tgz", + "integrity": "sha512-NkxsAxFWwsPsQiwFG2MzJ/T7uIR6AQNh1SzcxSUnmmIqIQMlLRQDKhc17M7IYjiuBXhrQRjQTo3CxX+DobS93g==", + "dependencies": { + "@smithy/types": "^4.8.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.3.3.tgz", + "integrity": "sha512-9f9Ixej0hFhroOK2TxZfUUDR13WVa8tQzhSzPDgXe5jGL3KmaM9s8XN7RQwqtEypI82q9KHnKS71CJ+q/1xLtQ==", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.3.tgz", + "integrity": "sha512-CmSlUy+eEYbIEYN5N3vvQTRfqt0lJlQkaQUIf+oizu7BbDut0pozfDjBGecfcfWf7c62Yis4JIEgqQ/TCfodaA==", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.1.tgz", + "integrity": "sha512-Ngb95ryR5A9xqvQFT5mAmYkCwbXvoLavLFwmi7zVg/IowFPCfiqRfkOKnbc/ZRL8ZKJ4f+Tp6kSu6wjDQb8L/g==", + "dependencies": { + "@smithy/core": "^3.17.1", + "@smithy/middleware-endpoint": "^4.3.5", + "@smithy/middleware-stack": "^4.2.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "@smithy/util-stream": "^4.5.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.8.0.tgz", + "integrity": "sha512-QpELEHLO8SsQVtqP+MkEgCYTFW0pleGozfs3cZ183ZBj9z3VC1CX1/wtFMK64p+5bhtZo41SeLK1rBRtd25nHQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.3.tgz", + "integrity": "sha512-I066AigYvY3d9VlU3zG9XzZg1yT10aNqvCaBTw9EPgu5GrsEl1aUkcMvhkIXascYH1A8W0LQo3B1Kr1cJNcQEw==", + "dependencies": { + "@smithy/querystring-parser": "^4.2.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", + "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", + "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", + "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.4.tgz", + "integrity": "sha512-qI5PJSW52rnutos8Bln8nwQZRpyoSRN6k2ajyoUHNMUzmWqHnOJCnDELJuV6m5PML0VkHI+XcXzdB+6awiqYUw==", + "dependencies": { + "@smithy/property-provider": "^4.2.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.6.tgz", + "integrity": "sha512-c6M/ceBTm31YdcFpgfgQAJaw3KbaLuRKnAz91iMWFLSrgxRpYm03c3bu5cpYojNMfkV9arCUelelKA7XQT36SQ==", + "dependencies": { + "@smithy/config-resolver": "^4.4.0", + "@smithy/credential-provider-imds": "^4.2.3", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.3.tgz", + "integrity": "sha512-aCfxUOVv0CzBIkU10TubdgKSx5uRvzH064kaiPEWfNIvKOtNpu642P4FP1hgOFkjQIkDObrfIDnKMKkeyrejvQ==", + "dependencies": { + "@smithy/node-config-provider": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.3.tgz", + "integrity": "sha512-v5ObKlSe8PWUHCqEiX2fy1gNv6goiw6E5I/PN2aXg3Fb/hse0xeaAnSpXDiWl7x6LamVKq7senB+m5LOYHUAHw==", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.3.tgz", + "integrity": "sha512-lLPWnakjC0q9z+OtiXk+9RPQiYPNAovt2IXD3CP4LkOnd9NpUsxOjMx1SnoUVB7Orb7fZp67cQMtTBKMFDvOGg==", + "dependencies": { + "@smithy/service-error-classification": "^4.2.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.4.tgz", + "integrity": "sha512-+qDxSkiErejw1BAIXUFBSfM5xh3arbz1MmxlbMCKanDDZtVEQ7PSKW9FQS0Vud1eI/kYn0oCTVKyNzRlq+9MUw==", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.4", + "@smithy/node-http-handler": "^4.4.3", + "@smithy/types": "^4.8.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", + "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.3.tgz", + "integrity": "sha512-5+nU///E5sAdD7t3hs4uwvCTWQtTR8JwKwOCSJtBRx0bY1isDo1QwH87vRK86vlFLBTISqoDA2V6xvP6nF1isQ==", + "dependencies": { + "@smithy/abort-controller": "^4.2.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", + "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==" + }, + "node_modules/@supabase/auth-js": { + "version": "2.76.1", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.76.1.tgz", + "integrity": "sha512-bxmcgPuyjTUBg7+jAohJ15TDh3ph4hXcv7QkRsQgnIpszurD5LYaJPzX638ETQ8zDL4fvHZRHfGrcmHV8C91jA==", + "dependencies": { + "@supabase/node-fetch": "2.6.15", + "tslib": "2.8.1" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.76.1", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.76.1.tgz", + "integrity": "sha512-+zJym/GC1sofm5QYKGxHSszCpMW4Ao2dj/WC3YlffAGuIlIhUtWTJvKsv5q7sWaSKUKdDhGpWhZ2OD++fW5BtQ==", + "dependencies": { + "@supabase/node-fetch": "2.6.15", + "tslib": "2.8.1" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.76.1", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.76.1.tgz", + "integrity": "sha512-QJ1Cwim6L9gzWKP8U4Lgw9x/4lMWkZSVMDRYFCH+vVGitVbtfU885swTiioOjjUe4EYGZm+Xktg90twzSVv6IA==", + "dependencies": { + "@supabase/node-fetch": "2.6.15", + "tslib": "2.8.1" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.76.1", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.76.1.tgz", + "integrity": "sha512-B5Lfmprea2fx2FS7obp4uAWiRUlEa6j9J3+BvvETGp/2LdkSRBaLEJCBylfcZTXk67ajNPX6ppvKvAZsckqXYg==", + "dependencies": { + "@supabase/node-fetch": "2.6.15", + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.76.1", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.76.1.tgz", + "integrity": "sha512-OJiNT8tocI9tcTjTjv1SBVLabzgEnS1NorZuqivkiJ0gTYmeg2c2PFmqCARhoQ4whF6zR9MVsX/Mtj2oSv4i/w==", + "dependencies": { + "@supabase/node-fetch": "2.6.15", + "tslib": "2.8.1" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.76.1", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.76.1.tgz", + "integrity": "sha512-dYMh9EsTVXZ6WbQ0QmMGIhbXct5+x636tXXaaxUmwjj3kY1jyBTQU8QehxAIfjyRu1mWGV07hoYmTYakkxdSGQ==", + "dependencies": { + "@supabase/auth-js": "2.76.1", + "@supabase/functions-js": "2.76.1", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "2.76.1", + "@supabase/realtime-js": "2.76.1", + "@supabase/storage-js": "2.76.1" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tiptap/core": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.6.5.tgz", + "integrity": "sha512-CgXuhevQbBcPfxaXzGZgIY9+aVMSAd68Q21g3EONz1iZBw026QgiaLhGK6jgGTErZL4GoNL/P+gC5nFCvN7+cA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "^3.6.5" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.6.3.tgz", + "integrity": "sha512-jnRCBLmGw8DFKYeG+vLO9C3YD90UzJoloP/xWcNcuJXohOXTs5+UzkRfbWM9jvvDCs+HNT+qz3NtirdoWt8ilA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.6.3.tgz", + "integrity": "sha512-En57aNnhb6N6enoq2JhrTyDr1FfU/ZaKuJk5Vbw1S/eXjvSDKZz6puWA2LAFKpk6KnfVpoy1nv+hVt97y0s3Xw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.6.3.tgz", + "integrity": "sha512-xariDZbj/bjZbTWkB1isxIufA8pN+CTwiCZAiU8V3ViQZ/baNBcQ2lQckXIbFpb1UoaKnKd8XBkh8SkcXXE8Tw==", + "optional": true, + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.6.3", + "@tiptap/pm": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.6.3.tgz", + "integrity": "sha512-SwTF/D5TYI0etjc0yt8eiSjMJ1fnu9w37Vis9ZwS+7VHIUIquSMon51YrXVLPA2+aBdTIMabevqtwyr3hh3CAg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.6.3.tgz", + "integrity": "sha512-sbTCVW58gJY+1Z9685rCi3luFRmv6qqm1EGc5XWj3UlkLAatfpeq4+PKgY09f7w35hRU60RAWt0VIyysU/b8ZQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.6.3.tgz", + "integrity": "sha512-RKI64JiOmy/guUXLe5rh2jD4dPG5678Fixkn+gDklMzRPFCwa7lvhRpTIJIzqqaR3ApGSqOforWQZcsiC5vrwg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.6.3", + "@tiptap/pm": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.6.3.tgz", + "integrity": "sha512-2GDXazz5NtFkmZ4z3h9vpQ2ngu/cVpmveqd6GFO4m8Zh40GOc3DvrZzAdHSHMJCf6ZB6ZIwOwretKJo4d9sLiA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.6.3.tgz", + "integrity": "sha512-3qlybvL9kSN4VUoxcmXJu5USoP/5P5OBqhAzPuE3UVFL9PZH/Zj6oY6ugXhUzLSpqPsd7117krLiXWuSWvR3fQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.6.3.tgz", + "integrity": "sha512-mE4MSkSOuFrvvKrI5CZFNRviJ8j1WfTJzXlXF8P8lqKtDAXveL2E2CuhSXAOoFuhgl1bHrLuZY46kveLZ4h8bg==", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "^3.6.3", + "@tiptap/pm": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.6.3.tgz", + "integrity": "sha512-fwg/a4cC7pKrjBkqxwCoBj2qUzcN7/mS5eqDF44YJrVMqILu6OqaBoRkA+7qK+iojTx9LOe3ngIVn1dBUAgBOQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.6.3.tgz", + "integrity": "sha512-LlN35hr2/od0tbL3cqyCuO7ZhzRBXnlUrV/Exg8DYvGfrmeCzFlduTYyCAyJwxD7fprX6R7irWf0t0qTITVtQQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.6.3.tgz", + "integrity": "sha512-yJgc3Ohu1k8xbaYez7gYBJ/9rWDdBe3kKQkOlzd+b9ZQL6cBpaWIMS7Two5qKmLmtCCaNYPnALvVGnrEmwvQaw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-highlight": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-3.6.3.tgz", + "integrity": "sha512-XTBqCN7vOjUDXBXQGvMHGbTDJoXHaD4v+2p/5ROKi6OzhJ4liuKfYqooir6SzNVwz7PgvzX2tsWzX2VAfLRj0g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.6.3.tgz", + "integrity": "sha512-7Ahasayoy73qpiKG85bdOiOUb5+FVe/zg2JOqwVyDVuRiW36xXPryD7k3WOIpyTBRPzdQ+rwDqwCBfHNeEwj/w==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.6.3", + "@tiptap/pm": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-image": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.6.3.tgz", + "integrity": "sha512-4CxAntkSLQQxVMcgPauGfnyrA7gzOBAAY6hHF49qAPPKNt4qpEbXny1SeUMm3bW/f592UiJ/XO45Fiv3lBd7EQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.6.3.tgz", + "integrity": "sha512-lq6yMG0rLQsVvfLrWL4Fb9dxmsGluehwduzbSPJjT1OZ+TGy/oPEhqteqsVej0sIP3V9byq55hfpuzrUi7NTSg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.6.3.tgz", + "integrity": "sha512-Lhv0HgoxCq6Y6ADS0273SAW79AyEs71CvLdZ8sf9ibinBHRBlBAHozUP8shYYXSurhPRMNJw0VmQqJZse/QZww==", + "dependencies": { + "linkifyjs": "^4.3.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.6.3", + "@tiptap/pm": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-list": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.6.3.tgz", + "integrity": "sha512-IkcvIJSXSJGaOrH+SPVfpYB4bDQ9bxTkJwyLvnAXmNao4CfXRAQznOjuxVYm/FOjdnOFLTB9gdFGbKj1qeglpA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.6.3", + "@tiptap/pm": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.6.3.tgz", + "integrity": "sha512-EL2xQfVwRbiz1lDZt63HAAkAFnzD1kyqIYS4hanfjtq7jg4b1sSplNq53Zy8fPMLtZOat9JQl/PNhfqRurQ5gw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-list-keymap": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.6.3.tgz", + "integrity": "sha512-bstO5HACdGR/6gVKkbFa+bn5e9sr0zOfbeYYePhKq2REX/hrJ/tpF+BaXgpEn8xtCCXOw0Zo3AwIIV9+LGlmcA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.6.3.tgz", + "integrity": "sha512-2WShxmzmqZVxWdACbTyqupDWLFCM7WI3dPisI1LVjjPeoKNqwdeAUqZSv5lXGA4HmxdOMbMI+qJ3j7zgQPx37g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.6.3.tgz", + "integrity": "sha512-E5HHEQFyEPVbZ5vyXo0CwxeNOBPyB+dEGUK5C8W5JFlWnO14kTtzuuHXkzOaCORoyQgUf/Z5SPLQEAIjLW2n6Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-placeholder": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.6.3.tgz", + "integrity": "sha512-a2aaPIAhiiACT0I4MdAkhZEBxAlkHAh9JQlYiivUlVh0MyB2a4LRlq32/zGthJKScJAi+TdYhEWCPkGzKA0slg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.6.3.tgz", + "integrity": "sha512-+qA+fcO0Cyxjjw6/WkMysY1eFXpA7MMUxH7em6j7zmQUxy4HL1cb2YL4YpFPxJTKhTfbvJ6Muag4mJ8zxDFe5w==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-table": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-3.6.3.tgz", + "integrity": "sha512-F5BtRIqGp2EZk1L9lqVNH/jKXWxaTHHesQow2KEKacR0SO2IcgQvZHYlac9CS6r94S8GiSkFLKlt3cdI0dnLeA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.6.3", + "@tiptap/pm": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-table-cell": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-3.6.3.tgz", + "integrity": "sha512-FKdTNTu6Wi8kGNQdWaUa7zBT9niBEkwT5GHi3K/6DFB1ur3C9QxFfUr6xDPyRMGJd7QvDZ7eQ3KfVk4nvxJ05g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-table": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-table-header": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-3.6.3.tgz", + "integrity": "sha512-JU4P24Dp1di8WLtqzatnTFeRa20YcN/ulKEhUHrg7Zxszrm5y1GIv/zW6bPWLxsnWSEeN2jYE+/k8GEv8zTGAw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-table": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-table-row": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-3.6.3.tgz", + "integrity": "sha512-BC8Q4dcGbr6y2JjATzE+vH4sJcx8BRh3hJhyg+aE0SIAgbhvde2NqVgtiynnITZxMWG0XH1wkXcSVzXaM1H/Jg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-table": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-task-item": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-task-item/-/extension-task-item-3.6.3.tgz", + "integrity": "sha512-uS6SqVZSPcOZqQ2S5hlr3lu0ObXqbMFyuzFVpuwblJcspaFPwYdiHSkqFBxNZxm2WWOA39tXvrfrlLlgV52o2A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-task-list": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-task-list/-/extension-task-list-3.6.3.tgz", + "integrity": "sha512-TK7UT/Nm971e7oZhyCeAcJAbNw84IzlgqZOP7CXTCRSZbFR3w0EQVwzeF+XUvFD+49bRNlU9sXZVQPkMiIVa4A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.6.3.tgz", + "integrity": "sha512-HetVEdKcWD6ZewBf/tECtPe7hDFwBLaeGgT/ptFuxL8LhQhkmJxo7Re3YXFJ25zAOnRQkFFoG2pbL5yihKw4PA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-text-align": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-3.6.3.tgz", + "integrity": "sha512-7qXSxpf0o+fQcmBHdYZbevF8SFYw/YU0EjQqDgr67jzb/LBnB3EKhvQ1qEC3c6GbZI1/W7a1Of+wSjHVJiXqkQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.6.3" + } + }, + "node_modules/@tiptap/extension-underline": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.6.3.tgz", + "integrity": "sha512-2gU/uoK6ZVAPyRNky9e1plARuEHNAO+GesmawAAG7O8MCK7D4iVEmutQ/5nAw3/pRZg5H21bdcqgq1vjTJaFRA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.6.3" + } + }, + "node_modules/@tiptap/extensions": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.6.3.tgz", + "integrity": "sha512-a38bFJ+g0BPoDXNwXIIK4oAM2pw34+AlIvrK9P9BFDammJXTskKPSwtN3sIWXEZpI1f/hoE4+1v64dGWAqrWvA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.6.3", + "@tiptap/pm": "^3.6.3" + } + }, + "node_modules/@tiptap/pm": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.6.5.tgz", + "integrity": "sha512-S+j6MPgUXRIQd5/mdaLjaJnOt4ptFwjqGjGMUfBbf9a3uKpXUXaCCzfuC6ZikwaUtoVh4KN9BU3HCYDtgtENPA==", + "dependencies": { + "prosemirror-changeset": "^2.3.0", + "prosemirror-collab": "^1.3.1", + "prosemirror-commands": "^1.6.2", + "prosemirror-dropcursor": "^1.8.1", + "prosemirror-gapcursor": "^1.3.2", + "prosemirror-history": "^1.4.1", + "prosemirror-inputrules": "^1.4.0", + "prosemirror-keymap": "^1.2.2", + "prosemirror-markdown": "^1.13.1", + "prosemirror-menu": "^1.2.4", + "prosemirror-model": "^1.24.1", + "prosemirror-schema-basic": "^1.2.3", + "prosemirror-schema-list": "^1.5.0", + "prosemirror-state": "^1.4.3", + "prosemirror-tables": "^1.6.4", + "prosemirror-trailing-node": "^3.0.0", + "prosemirror-transform": "^1.10.2", + "prosemirror-view": "^1.38.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/react": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.6.3.tgz", + "integrity": "sha512-y9laShSXRoHIjf6UlWIEbGYk9knET3Zr+COjMOj9enAadXHDU8d18LaALMhRadJUVYpxs7sjsmrdIdME3T7WvA==", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "fast-deep-equal": "^3.1.3", + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "optionalDependencies": { + "@tiptap/extension-bubble-menu": "^3.6.3", + "@tiptap/extension-floating-menu": "^3.6.3" + }, + "peerDependencies": { + "@tiptap/core": "^3.6.3", + "@tiptap/pm": "^3.6.3", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tiptap/starter-kit": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.6.3.tgz", + "integrity": "sha512-GHQNUyBlhYjWzBLsyrnlQppeAnRCD9k45i4l93g2Re+WdqYrgfSPdbV5Ixhl+fDXVqs/s7sPqADFPbzKEwfkzA==", + "dependencies": { + "@tiptap/core": "^3.6.3", + "@tiptap/extension-blockquote": "^3.6.3", + "@tiptap/extension-bold": "^3.6.3", + "@tiptap/extension-bullet-list": "^3.6.3", + "@tiptap/extension-code": "^3.6.3", + "@tiptap/extension-code-block": "^3.6.3", + "@tiptap/extension-document": "^3.6.3", + "@tiptap/extension-dropcursor": "^3.6.3", + "@tiptap/extension-gapcursor": "^3.6.3", + "@tiptap/extension-hard-break": "^3.6.3", + "@tiptap/extension-heading": "^3.6.3", + "@tiptap/extension-horizontal-rule": "^3.6.3", + "@tiptap/extension-italic": "^3.6.3", + "@tiptap/extension-link": "^3.6.3", + "@tiptap/extension-list": "^3.6.3", + "@tiptap/extension-list-item": "^3.6.3", + "@tiptap/extension-list-keymap": "^3.6.3", + "@tiptap/extension-ordered-list": "^3.6.3", + "@tiptap/extension-paragraph": "^3.6.3", + "@tiptap/extension-strike": "^3.6.3", + "@tiptap/extension-text": "^3.6.3", + "@tiptap/extension-underline": "^3.6.3", + "@tiptap/extensions": "^3.6.3", + "@tiptap/pm": "^3.6.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@types/bcryptjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-3.0.0.tgz", + "integrity": "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==", + "deprecated": "This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "bcryptjs": "*" + } + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.12.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz", + "integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==" + }, + "node_modules/@types/react": { + "version": "18.3.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.2.tgz", + "integrity": "sha512-Btgg89dAnqD4vV7R3hlwOxgqobUQKgx3MmrQRi0yYbs/P0ym8XozIAlkqVilPqHQwXs4e9Tf63rrCgl58BcO4w==", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.12.tgz", + "integrity": "sha512-vAPMQdnyKCBtkmQA6FMCBvU9qFIppS3nzyXnEM+Lo2IAhG4Mpjv9cCxMudhgV3YdNNJv6TNqXy97dfRVL2LmaQ==", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "node_modules/bowser": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", + "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==" + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/c12/node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/c12/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001746", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001746.tgz", + "integrity": "sha512-eA7Ys/DGw+pnkWWSE/id29f2IcPHVoE8wxtvE5JdvD2V28VTDPy1yEeo11Guz0sJ4ZeGRcm3uaTcAqK1LXaphA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==" + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/effect": { + "version": "3.16.12", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.16.12.tgz", + "integrity": "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.230", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.230.tgz", + "integrity": "sha512-A6A6Fd3+gMdaed9wX83CvHYJb4UuapPD5X5SLq72VZJzxHSY0/LUweGXRWmQlh2ln7KV7iw7jnwXK7dlPoOnHQ==" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": { + "he": "bin/he" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/linkifyjs": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", + "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/lucide-react": { + "version": "0.544.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.544.0.tgz", + "integrity": "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/monaco-editor": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.47.0.tgz", + "integrity": "sha512-VabVvHvQ9QmMwXu4du008ZDuyLnHs9j7ThVFsiJoXSOQk18+LF89N4ADzPbFenm0W4V2bGHnFBztIRQTgBfxzw==" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.4.tgz", + "integrity": "sha512-xH4Yjhb82sFYQfY3vbkJfgSDgXvBB6a8xPs9i35k6oZJRoQRihZH+4s9Yo2qsWpzBmZ3lPXaJ2KPXLfkvW4LnA==", + "dependencies": { + "@next/env": "15.5.4", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.4", + "@next/swc-darwin-x64": "15.5.4", + "@next/swc-linux-arm64-gnu": "15.5.4", + "@next/swc-linux-arm64-musl": "15.5.4", + "@next/swc-linux-x64-gnu": "15.5.4", + "@next/swc-linux-x64-musl": "15.5.4", + "@next/swc-win32-arm64-msvc": "15.5.4", + "@next/swc-win32-x64-msvc": "15.5.4", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/nextjs-toploader": { + "version": "3.9.17", + "resolved": "https://registry.npmjs.org/nextjs-toploader/-/nextjs-toploader-3.9.17.tgz", + "integrity": "sha512-9OF0KSSLtoSAuNg2LZ3aTl4hR9mBDj5L9s9DZiFCbMlXehyICGjkIz5dVGzuATU2bheJZoBdFgq9w07AKSuQQw==", + "license": "MIT", + "dependencies": { + "nprogress": "^0.2.0", + "prop-types": "^15.8.1" + }, + "funding": { + "url": "https://buymeacoffee.com/thesgj" + }, + "peerDependencies": { + "next": ">= 6.0.0", + "react": ">= 16.0.0", + "react-dom": ">= 16.0.0" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==" + }, + "node_modules/node-html-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-7.0.1.tgz", + "integrity": "sha512-KGtmPY2kS0thCWGK0VuPyOS+pBKhhe8gXztzA2ilAOhbUbxa9homF1bOyKvhGzMLXUoRds9IOmr/v5lr/lqNmA==", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", + "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nprogress": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", + "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nypm": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", + "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.2", + "pathe": "^2.0.3", + "pkg-types": "^2.3.0", + "tinyexec": "^1.0.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==" + }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/prisma": { + "version": "6.16.3", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.16.3.tgz", + "integrity": "sha512-4tJq3KB9WRshH5+QmzOLV54YMkNlKOtLKaSdvraI5kC/axF47HuOw6zDM8xrxJ6s9o2WodY654On4XKkrobQdQ==", + "hasInstallScript": true, + "dependencies": { + "@prisma/config": "6.16.3", + "@prisma/engines": "6.16.3" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prosemirror-changeset": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz", + "integrity": "sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-collab": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz", + "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==", + "dependencies": { + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", + "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.10.2" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz", + "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz", + "integrity": "sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.4.1.tgz", + "integrity": "sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.0.tgz", + "integrity": "sha512-K0xJRCmt+uSw7xesnHmcn72yBGTbY45vm8gXI4LZXbx2Z0jwh5aF9xrGQgrVPu0WbyFVFF3E/o9VhJYz6SQWnA==", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-markdown": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz", + "integrity": "sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==", + "dependencies": { + "@types/markdown-it": "^14.0.0", + "markdown-it": "^14.0.0", + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-menu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.5.tgz", + "integrity": "sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==", + "dependencies": { + "crelt": "^1.0.0", + "prosemirror-commands": "^1.0.0", + "prosemirror-history": "^1.0.0", + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.25.3", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.3.tgz", + "integrity": "sha512-dY2HdaNXlARknJbrManZ1WyUtos+AP97AmvqdOQtWtrrC5g4mohVX5DTi9rXNFSk09eczLq9GuNTtq3EfMeMGA==", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-basic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz", + "integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==", + "dependencies": { + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.3.tgz", + "integrity": "sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.1.tgz", + "integrity": "sha512-DAgDoUYHCcc6tOGpLVPSU1k84kCUWTWnfWX3UDy2Delv4ryH0KqTD6RBI6k4yi9j9I8gl3j8MkPpRD/vWPZbug==", + "dependencies": { + "prosemirror-keymap": "^1.2.2", + "prosemirror-model": "^1.25.0", + "prosemirror-state": "^1.4.3", + "prosemirror-transform": "^1.10.3", + "prosemirror-view": "^1.39.1" + } + }, + "node_modules/prosemirror-trailing-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz", + "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==", + "dependencies": { + "@remirror/core-constants": "3.0.0", + "escape-string-regexp": "^4.0.0" + }, + "peerDependencies": { + "prosemirror-model": "^1.22.1", + "prosemirror-state": "^1.4.2", + "prosemirror-view": "^1.33.8" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.4.tgz", + "integrity": "sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.2.tgz", + "integrity": "sha512-PGS/jETmh+Qjmre/6vcG7SNHAKiGc4vKOJmHMPRmvcUl7ISuVtrtHmH06UDUwaim4NDJfZfVMl7U7JkMMETa6g==", + "dependencies": { + "prosemirror-model": "^1.20.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz", + "integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.0", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.4", + "@img/sharp-darwin-x64": "0.34.4", + "@img/sharp-libvips-darwin-arm64": "1.2.3", + "@img/sharp-libvips-darwin-x64": "1.2.3", + "@img/sharp-libvips-linux-arm": "1.2.3", + "@img/sharp-libvips-linux-arm64": "1.2.3", + "@img/sharp-libvips-linux-ppc64": "1.2.3", + "@img/sharp-libvips-linux-s390x": "1.2.3", + "@img/sharp-libvips-linux-x64": "1.2.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", + "@img/sharp-libvips-linuxmusl-x64": "1.2.3", + "@img/sharp-linux-arm": "0.34.4", + "@img/sharp-linux-arm64": "0.34.4", + "@img/sharp-linux-ppc64": "0.34.4", + "@img/sharp-linux-s390x": "0.34.4", + "@img/sharp-linux-x64": "0.34.4", + "@img/sharp-linuxmusl-arm64": "0.34.4", + "@img/sharp-linuxmusl-x64": "0.34.4", + "@img/sharp-wasm32": "0.34.4", + "@img/sharp-win32-arm64": "0.34.4", + "@img/sharp-win32-ia32": "0.34.4", + "@img/sharp-win32-x64": "0.34.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ] + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", + "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", + "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/usehooks-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.1.tgz", + "integrity": "sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA==", + "dependencies": { + "lodash.debounce": "^4.0.8" + }, + "engines": { + "node": ">=16.15.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/zod": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", + "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/nextjs/package.json b/nextjs/package.json new file mode 100644 index 0000000..6052bb1 --- /dev/null +++ b/nextjs/package.json @@ -0,0 +1,66 @@ +{ + "name": "notion-clone", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.918.0", + "@aws-sdk/s3-request-presigner": "^3.918.0", + "@monaco-editor/react": "^4.6.0", + "@prisma/client": "^6.16.3", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-slot": "^1.0.2", + "@supabase/supabase-js": "^2.76.1", + "@tiptap/core": "^3.6.3", + "@tiptap/extension-highlight": "^3.6.3", + "@tiptap/extension-image": "^3.6.3", + "@tiptap/extension-link": "^3.6.3", + "@tiptap/extension-placeholder": "^3.6.3", + "@tiptap/extension-strike": "^3.6.3", + "@tiptap/extension-table": "^3.6.3", + "@tiptap/extension-table-cell": "^3.6.3", + "@tiptap/extension-table-header": "^3.6.3", + "@tiptap/extension-table-row": "^3.6.3", + "@tiptap/extension-task-item": "^3.6.3", + "@tiptap/extension-task-list": "^3.6.3", + "@tiptap/extension-text-align": "^3.6.3", + "@tiptap/extension-underline": "^3.6.3", + "@tiptap/react": "^3.6.3", + "@tiptap/starter-kit": "^3.6.3", + "autoprefixer": "^10.4.21", + "bcryptjs": "^3.0.2", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.0", + "jsonwebtoken": "^9.0.2", + "lucide-react": "^0.544.0", + "monaco-editor": "^0.47.0", + "next": "^15.5.4", + "next-themes": "^0.4.6", + "nextjs-toploader": "^3.9.17", + "node-html-parser": "^7.0.1", + "prisma": "^6.16.3", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "tailwind-merge": "^2.3.0", + "tailwindcss-animate": "^1.0.7", + "usehooks-ts": "^3.1.0", + "zod": "^4.1.12", + "zustand": "^5.0.8" + }, + "devDependencies": { + "@types/bcryptjs": "^3.0.0", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "20.12.11", + "@types/react": "18.3.2", + "@types/react-dom": "^18", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } +} diff --git a/nextjs/postcss.config.mjs b/nextjs/postcss.config.mjs new file mode 100644 index 0000000..2ef30fc --- /dev/null +++ b/nextjs/postcss.config.mjs @@ -0,0 +1,9 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; + +export default config; diff --git a/nextjs/prisma/schema.prisma b/nextjs/prisma/schema.prisma new file mode 100644 index 0000000..eadc55f --- /dev/null +++ b/nextjs/prisma/schema.prisma @@ -0,0 +1,131 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" + output = "../node_modules/.prisma/client" +} + +datasource db { + provider = "mysql" + url = env("DATABASE_URL") +} + +// User model for authentication +model User { + id String @id @default(uuid()) + email String @unique + name String? + image String? + password String // Hashed password + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Relations + documents Document[] + folders Folder[] + templates Template[] + + @@map("users") +} + +// Folder model for organizing documents +model Folder { + id String @id @default(uuid()) + name String + icon String? // Folder icon + color String? // Folder color + isArchived Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Hierarchy + parentId String? + parent Folder? @relation("FolderHierarchy", fields: [parentId], references: [id]) + children Folder[] @relation("FolderHierarchy") + + // Ownership + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // Relations + documents Document[] + + // Indexes + @@index([userId]) + @@index([parentId]) + @@index([isArchived]) + @@map("folders") +} + +// Document model for Notion-like documents +model Document { + id String @id @default(uuid()) + title String + content Json? // JSON content for rich text + icon String? // Document icon + cover String? // Cover image URL + isPublished Boolean @default(false) + isArchived Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Document type + type DocumentType @default(PAGE) // PAGE, CODE_FILE + + // Code file specific fields + filePath String? // Path within the repository + fileContent String? // Raw file content + language String? // Programming language + fileSize Int? // File size in bytes + + // Hierarchy + parentId String? + parent Document? @relation("DocumentHierarchy", fields: [parentId], references: [id]) + children Document[] @relation("DocumentHierarchy") + + // Folder relationship + folderId String? + folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull) + + // Ownership + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // Indexes + @@index([userId]) + @@index([parentId]) + @@index([folderId]) + @@index([isArchived]) + @@index([type]) + @@map("documents") +} + +// Template model for document templates +model Template { + id String @id @default(uuid()) + name String + description String? + category String @default("General") + title String + content Json // JSON content for rich text + isPublic Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Ownership + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // Indexes + @@index([userId]) + @@index([category]) + @@index([isPublic]) + @@map("templates") +} + +// Document type enum +enum DocumentType { + PAGE + CODE_FILE +} diff --git a/nextjs/public/documents.png b/nextjs/public/documents.png new file mode 100644 index 0000000..024d63d Binary files /dev/null and b/nextjs/public/documents.png differ diff --git a/nextjs/public/documents_dark.png b/nextjs/public/documents_dark.png new file mode 100644 index 0000000..9cf4806 Binary files /dev/null and b/nextjs/public/documents_dark.png differ diff --git a/nextjs/public/notionIcon.png b/nextjs/public/notionIcon.png new file mode 100644 index 0000000..c3951a8 Binary files /dev/null and b/nextjs/public/notionIcon.png differ diff --git a/nextjs/public/notionIcond.png b/nextjs/public/notionIcond.png new file mode 100644 index 0000000..3910516 Binary files /dev/null and b/nextjs/public/notionIcond.png differ diff --git a/nextjs/src/app/config/index.ts b/nextjs/src/app/config/index.ts new file mode 100644 index 0000000..48f13b3 --- /dev/null +++ b/nextjs/src/app/config/index.ts @@ -0,0 +1,3 @@ +// App configuration exports +export * from './metadata' + diff --git a/nextjs/src/app/config/metadata.ts b/nextjs/src/app/config/metadata.ts new file mode 100644 index 0000000..f75c9c1 --- /dev/null +++ b/nextjs/src/app/config/metadata.ts @@ -0,0 +1,21 @@ +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Jotion", + description: "Generated by Jotion", + icons: { + icon: [ + { + media: "(prefers-color-scheme: light)", + url: "/next.svg", + href: "/next.svg", + }, + { + media: "(prefers-color-scheme: dark)", + url: "/next.svg", + href: "/next.svg", + } + ] + } +}; + diff --git a/nextjs/src/app/favicon.ico b/nextjs/src/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/nextjs/src/app/favicon.ico differ diff --git a/nextjs/src/app/globals.css b/nextjs/src/app/globals.css new file mode 100644 index 0000000..5890da7 --- /dev/null +++ b/nextjs/src/app/globals.css @@ -0,0 +1,321 @@ +@tailwind base; + @tailwind components; + @tailwind utilities; + + /* Hide scrollbar but keep functionality */ + .scrollbar-hide { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } + .scrollbar-hide::-webkit-scrollbar { + display: none; /* Chrome, Safari and Opera */ + } + + html, + body, + :root { + height: 100%; + } + + @layer base { + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + + --radius: 0.5rem; + } + + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + } + } + + @layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } + } + + /* Tiptap Editor Styles */ + .ProseMirror { + outline: none; + min-height: 200px; + color: #1f2937; /* Default text color for light theme */ + } + + /* Dark theme text color */ + .dark .ProseMirror, + .dark .ProseMirror p, + .dark .ProseMirror div, + .dark .ProseMirror span { + color: #ffffff !important; /* 완전한 하얀색 강제 적용 */ + } + + .ProseMirror p.is-editor-empty:first-child::before { + color: #adb5bd; + content: attr(data-placeholder); + float: left; + height: 0; + pointer-events: none; + } + + /* Dark theme placeholder */ + .dark .ProseMirror p.is-editor-empty:first-child::before { + color: #6b7280; + } + + .ProseMirror h1 { + font-size: 2em; + font-weight: bold; + margin: 0.67em 0; + color: #111827; + } + + .ProseMirror h2 { + font-size: 1.5em; + font-weight: bold; + margin: 0.83em 0; + color: #111827; + } + + .ProseMirror h3 { + font-size: 1.17em; + font-weight: bold; + margin: 1em 0; + color: #111827; + } + + /* Dark theme headings */ + .dark .ProseMirror h1, + .dark .ProseMirror h2, + .dark .ProseMirror h3 { + color: #f9fafb; + } + + .ProseMirror ul, + .ProseMirror ol { + padding-left: 1.5em; + color: inherit; + } + + .ProseMirror li { + color: inherit; + } + + .ProseMirror blockquote { + border-left: 3px solid #e2e8f0; + padding-left: 1rem; + margin: 1rem 0; + font-style: italic; + color: #6b7280; + } + + /* Dark theme blockquote */ + .dark .ProseMirror blockquote { + color: #ffffff; /* 완전한 하얀색 */ + } + + .ProseMirror code { + background-color: #f1f5f9; + border-radius: 0.25rem; + padding: 0.125rem 0.25rem; + font-family: 'Courier New', monospace; + color: #374151; + } + + /* Dark theme inline code */ + .dark .ProseMirror code { + background-color: #374151; + color: #f3f4f6; + } + + .ProseMirror pre { + background-color: #1e293b; + color: #e2e8f0; + border-radius: 0.5rem; + padding: 1rem; + overflow-x: auto; + margin: 1rem 0; + } + + .ProseMirror pre code { + background: none; + padding: 0; + color: inherit; + } + + .ProseMirror table { + border-collapse: collapse; + margin: 1rem 0; + width: 100%; + } + + .ProseMirror th, + .ProseMirror td { + border: 1px solid #e2e8f0; + padding: 0.5rem; + text-align: left; + } + + .ProseMirror th { + background-color: #f8fafc; + font-weight: bold; + } + + .ProseMirror img { + max-width: 100%; + height: auto; + border-radius: 0.5rem; + } + + .ProseMirror a { + color: #3b82f6; + text-decoration: underline; + } + + /* Dark theme links */ + .dark .ProseMirror a { + color: #60a5fa; + } + + .ProseMirror mark { + background-color: #fef08a; + padding: 0.125rem 0.25rem; + border-radius: 0.25rem; + } + + /* Dark theme highlight */ + .dark .ProseMirror mark { + background-color: #fbbf24; + color: #111827; + } + + .ProseMirror ul[data-type="taskList"] { + list-style: none; + padding: 0; + } + + .ProseMirror ul[data-type="taskList"] li { + display: flex; + align-items: flex-start; + } + + .ProseMirror ul[data-type="taskList"] li > label { + flex: 0 0 auto; + margin-right: 0.5rem; + user-select: none; + } + + .ProseMirror ul[data-type="taskList"] li > div { + flex: 1 1 auto; + } + + .ProseMirror ul[data-type="taskList"] input[type="checkbox"] { + cursor: pointer; + } + + /* Dark mode styles */ + .dark .ProseMirror blockquote { + border-left-color: #475569; + } + + .dark .ProseMirror code { + background-color: #334155; + } + + .dark .ProseMirror th { + background-color: #1e293b; + } + + .dark .ProseMirror th, + .dark .ProseMirror td { + border-color: #475569; + } + + /* Line clamp utilities */ + .line-clamp-2 { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } + + .line-clamp-3 { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + } + + /* Code block styles */ + pre { + display: block; + overflow-x: auto; + padding: 1rem; + background: #1e1e1e; + color: #d4d4d4; + border-radius: 0.5rem; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.875rem; + line-height: 1.5; + } + + /* Light mode code block */ + .light pre { + background: #f8f8f8; + color: #333; + } \ No newline at end of file diff --git a/nextjs/src/app/providers/auth-provider.tsx b/nextjs/src/app/providers/auth-provider.tsx new file mode 100644 index 0000000..1f9a536 --- /dev/null +++ b/nextjs/src/app/providers/auth-provider.tsx @@ -0,0 +1,72 @@ +"use client" + +import { createContext, useContext, useEffect, useState } from "react" +import { useRouter } from "next/navigation" +import type { User, AuthContextType } from "@/shared/types" + +const AuthContext = createContext(undefined) + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState(null) + const [isLoading, setIsLoading] = useState(true) + const router = useRouter() + + useEffect(() => { + // Check for existing session via API + checkAuth() + }, []) + + const checkAuth = async () => { + try { + const response = await fetch('/api/auth/me', { + credentials: 'include', + }) + + if (response.ok) { + const userData = await response.json() + setUser(userData) + } else { + setUser(null) + } + } catch (error) { + console.error("Error checking auth:", error) + setUser(null) + } finally { + setIsLoading(false) + } + } + + const login = async (token: string, userData: User) => { + // Token is already set in cookie by login API + setUser(userData) + } + + const logout = async () => { + try { + // Clear auth cookie by calling logout API + await fetch('/api/auth/logout', { + method: 'POST', + credentials: 'include', + }) + } catch (error) { + console.error("Error during logout:", error) + } + setUser(null) + router.push("/home") + } + + return ( + + {children} + + ) +} + +export function useAuth() { + const context = useContext(AuthContext) + if (context === undefined) { + throw new Error("useAuth must be used within an AuthProvider") + } + return context +} + diff --git a/nextjs/src/app/providers/theme-provider.tsx b/nextjs/src/app/providers/theme-provider.tsx new file mode 100644 index 0000000..7a48ad0 --- /dev/null +++ b/nextjs/src/app/providers/theme-provider.tsx @@ -0,0 +1,10 @@ +"use client" + +import * as React from "react" +import { ThemeProvider as NextThemesProvider } from "next-themes" +import { type ThemeProviderProps } from "next-themes" + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children} +} + diff --git a/nextjs/src/entities/document/api/index.ts b/nextjs/src/entities/document/api/index.ts new file mode 100644 index 0000000..bddafae --- /dev/null +++ b/nextjs/src/entities/document/api/index.ts @@ -0,0 +1,67 @@ +import type { Document, DocumentWithRelations } from '../model/types'; +import type { DocumentInput } from '../model/validation'; +import { apiGet, apiPost, apiPut, apiDelete } from '@/shared/api'; + +// Document API functions +export const documentApi = { + // Get all documents + getDocuments: async (): Promise => { + try { + return await apiGet('/api/documents'); + } catch (error) { + console.error('Failed to fetch documents:', error); + throw error; + } + }, + + // Get a specific document + getDocument: async (id: string): Promise => { + try { + return await apiGet(`/api/documents/${id}`); + } catch (error) { + console.error('Failed to fetch document:', error); + throw error; + } + }, + + // Create a new document + createDocument: async (newDocument: DocumentInput): Promise => { + try { + return await apiPost('/api/documents', newDocument); + } catch (error) { + console.error('Failed to create document:', error); + throw error; + } + }, + + // Update a document + updateDocument: async (id: string, updateData: Partial): Promise => { + try { + return await apiPut(`/api/documents/${id}`, updateData); + } catch (error) { + console.error('Failed to update document:', error); + throw error; + } + }, + + // Delete a document + deleteDocument: async (id: string): Promise => { + try { + await apiDelete(`/api/documents/${id}`); + } catch (error) { + console.error('Failed to delete document:', error); + throw error; + } + }, + + // Get public document + getPublicDocument: async (id: string): Promise => { + try { + return await apiGet(`/api/documents/${id}/public`); + } catch (error) { + console.error('Failed to fetch public document:', error); + throw error; + } + }, +}; + diff --git a/nextjs/src/entities/document/index.ts b/nextjs/src/entities/document/index.ts new file mode 100644 index 0000000..3062c4b --- /dev/null +++ b/nextjs/src/entities/document/index.ts @@ -0,0 +1,4 @@ +export * from './api'; +export * from './model'; +export * from './ui'; + diff --git a/nextjs/src/entities/document/model/index.ts b/nextjs/src/entities/document/model/index.ts new file mode 100644 index 0000000..5b7b829 --- /dev/null +++ b/nextjs/src/entities/document/model/index.ts @@ -0,0 +1,4 @@ +export * from './types'; +export * from './store'; +export * from './validation'; + diff --git a/nextjs/src/entities/document/model/store.ts b/nextjs/src/entities/document/model/store.ts new file mode 100644 index 0000000..bbb59f2 --- /dev/null +++ b/nextjs/src/entities/document/model/store.ts @@ -0,0 +1,36 @@ +import { create } from 'zustand'; +import { DocumentWithRelations, DocumentState, DocumentActions } from './types'; + +// Document state management store +export const useDocumentStore = create((set) => ({ + documents: [], + currentDocument: null, + isLoading: true, + + readDocuments: (documents: DocumentWithRelations[]) => set({ documents }), + + setCurrentDocument: (document: DocumentWithRelations | null) => set({ currentDocument: document }), + + createDocument: (newDocument: DocumentWithRelations) => set((state) => ({ + documents: [newDocument, ...state.documents] + })), + + updateDocument: (updatedDocument: DocumentWithRelations) => set((state) => ({ + documents: state.documents.map(doc => + doc.id === updatedDocument.id ? updatedDocument : doc + ), + currentDocument: state.currentDocument?.id === updatedDocument.id + ? updatedDocument + : state.currentDocument + })), + + deleteDocument: (documentId: string) => set((state) => ({ + documents: state.documents.filter(doc => doc.id !== documentId), + currentDocument: state.currentDocument?.id === documentId + ? null + : state.currentDocument + })), + + setLoading: (isLoading: boolean) => set({ isLoading }), +})); + diff --git a/nextjs/src/entities/document/model/types.ts b/nextjs/src/entities/document/model/types.ts new file mode 100644 index 0000000..84c5f9d --- /dev/null +++ b/nextjs/src/entities/document/model/types.ts @@ -0,0 +1,70 @@ +// Document entity types +export type DocumentType = 'PAGE' | 'CODE_FILE'; + +export interface Document { + id: string; + title: string; + content?: any; // JSON content for rich text + icon?: string; + cover?: string; + isPublished: boolean; + isArchived: boolean; + createdAt: Date | string; + updatedAt: Date | string; + type: DocumentType; + filePath?: string; + fileContent?: string; + language?: string; + fileSize?: number; + parentId?: string; + folderId?: string; + userId: string; +} + +export interface DocumentWithRelations extends Document { + parent?: Document; + children?: Document[]; + folder?: { + id: string; + name: string; + icon?: string; + }; + user?: any; + _count?: { + documents: number; + }; +} + +export interface DocumentListItem { + id: string; + title: string; + icon?: string; + updatedAt: string; +} + +export interface HeadingItem { + id: string; + text: string; + level: number; + element?: HTMLElement; + children?: HeadingItem[]; + isExpanded?: boolean; +} + +// Document state +export interface DocumentState { + documents: DocumentWithRelations[]; + currentDocument: DocumentWithRelations | null; + isLoading: boolean; +} + +// Document actions +export interface DocumentActions { + readDocuments: (documents: DocumentWithRelations[]) => void; + setCurrentDocument: (document: DocumentWithRelations | null) => void; + createDocument: (document: DocumentWithRelations) => void; + updateDocument: (updatedDocument: DocumentWithRelations) => void; + deleteDocument: (documentId: string) => void; + setLoading: (isLoading: boolean) => void; +} + diff --git a/nextjs/src/entities/document/model/validation.ts b/nextjs/src/entities/document/model/validation.ts new file mode 100644 index 0000000..ae7cd80 --- /dev/null +++ b/nextjs/src/entities/document/model/validation.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; + +// Document validation schemas +export const documentSchema = z.object({ + title: z.string().min(1, 'Title is required').max(200, 'Title is too long'), + content: z.any().optional(), + icon: z.string().optional(), + cover: z.string().optional(), + isPublished: z.boolean().default(false), + isArchived: z.boolean().default(false), + type: z.enum(['PAGE', 'CODE_FILE']).default('PAGE'), + filePath: z.string().optional(), + fileContent: z.string().optional(), + language: z.string().optional(), + fileSize: z.number().optional(), + parentId: z.string().optional(), + folderId: z.string().optional(), +}); + +export type DocumentInput = z.infer; + diff --git a/nextjs/src/entities/document/ui/DocumentItem.tsx b/nextjs/src/entities/document/ui/DocumentItem.tsx new file mode 100644 index 0000000..fd8a231 --- /dev/null +++ b/nextjs/src/entities/document/ui/DocumentItem.tsx @@ -0,0 +1,45 @@ +import { DocumentWithRelations } from '../model/types'; +import { cn } from '@/shared/lib/utils'; +import { FileText, Calendar, Folder } from 'lucide-react'; +import Link from 'next/link'; + +interface DocumentItemProps { + document: DocumentWithRelations; + className?: string; +} + +export const DocumentItem = ({ document, className }: DocumentItemProps) => { + const Icon = document.icon ? + {document.icon} : + ; + + return ( + +
+ {Icon} +
+
+

{document.title}

+
+ {document.folder && ( +
+ + {document.folder.name} +
+ )} +
+ + {new Date(document.updatedAt).toLocaleDateString()} +
+
+
+ + ); +}; + diff --git a/nextjs/src/entities/document/ui/index.ts b/nextjs/src/entities/document/ui/index.ts new file mode 100644 index 0000000..df03843 --- /dev/null +++ b/nextjs/src/entities/document/ui/index.ts @@ -0,0 +1,2 @@ +export * from './DocumentItem'; + diff --git a/nextjs/src/entities/folder/api/index.ts b/nextjs/src/entities/folder/api/index.ts new file mode 100644 index 0000000..f425727 --- /dev/null +++ b/nextjs/src/entities/folder/api/index.ts @@ -0,0 +1,57 @@ +import type { Folder, FolderWithRelations } from '../model/types'; +import type { FolderInput } from '../model/validation'; +import { apiGet, apiPost, apiPut, apiDelete } from '@/shared/api'; + +// Folder API functions +export const folderApi = { + // Get all folders + getFolders: async (): Promise => { + try { + return await apiGet('/api/folders'); + } catch (error) { + console.error('Failed to fetch folders:', error); + throw error; + } + }, + + // Get a specific folder + getFolder: async (id: string): Promise => { + try { + return await apiGet(`/api/folders/${id}`); + } catch (error) { + console.error('Failed to fetch folder:', error); + throw error; + } + }, + + // Create a new folder + createFolder: async (newFolder: FolderInput): Promise => { + try { + return await apiPost('/api/folders', newFolder); + } catch (error) { + console.error('Failed to create folder:', error); + throw error; + } + }, + + // Update a folder + updateFolder: async (id: string, updateData: Partial): Promise => { + try { + return await apiPut(`/api/folders/${id}`, updateData); + } catch (error) { + console.error('Failed to update folder:', error); + throw error; + } + }, + + // Delete a folder + deleteFolder: async (id: string): Promise => { + try { + await apiDelete(`/api/folders/${id}`); + } catch (error) { + console.error('Failed to delete folder:', error); + throw error; + } + }, +}; + diff --git a/nextjs/src/entities/folder/index.ts b/nextjs/src/entities/folder/index.ts new file mode 100644 index 0000000..3062c4b --- /dev/null +++ b/nextjs/src/entities/folder/index.ts @@ -0,0 +1,4 @@ +export * from './api'; +export * from './model'; +export * from './ui'; + diff --git a/nextjs/src/entities/folder/model/index.ts b/nextjs/src/entities/folder/model/index.ts new file mode 100644 index 0000000..5b7b829 --- /dev/null +++ b/nextjs/src/entities/folder/model/index.ts @@ -0,0 +1,4 @@ +export * from './types'; +export * from './store'; +export * from './validation'; + diff --git a/nextjs/src/entities/folder/model/store.ts b/nextjs/src/entities/folder/model/store.ts new file mode 100644 index 0000000..6b91ab6 --- /dev/null +++ b/nextjs/src/entities/folder/model/store.ts @@ -0,0 +1,36 @@ +import { create } from 'zustand'; +import { FolderWithRelations, FolderState, FolderActions } from './types'; + +// Folder state management store +export const useFolderStore = create((set) => ({ + folders: [], + currentFolder: null, + isLoading: true, + + readFolders: (folders: FolderWithRelations[]) => set({ folders }), + + setCurrentFolder: (folder: FolderWithRelations | null) => set({ currentFolder: folder }), + + createFolder: (newFolder: FolderWithRelations) => set((state) => ({ + folders: [newFolder, ...state.folders] + })), + + updateFolder: (updatedFolder: FolderWithRelations) => set((state) => ({ + folders: state.folders.map(folder => + folder.id === updatedFolder.id ? updatedFolder : folder + ), + currentFolder: state.currentFolder?.id === updatedFolder.id + ? updatedFolder + : state.currentFolder + })), + + deleteFolder: (folderId: string) => set((state) => ({ + folders: state.folders.filter(folder => folder.id !== folderId), + currentFolder: state.currentFolder?.id === folderId + ? null + : state.currentFolder + })), + + setLoading: (isLoading: boolean) => set({ isLoading }), +})); + diff --git a/nextjs/src/entities/folder/model/types.ts b/nextjs/src/entities/folder/model/types.ts new file mode 100644 index 0000000..3d71ef8 --- /dev/null +++ b/nextjs/src/entities/folder/model/types.ts @@ -0,0 +1,44 @@ +// Folder entity types +export interface Folder { + id: string; + name: string; + icon?: string; + color?: string; + isArchived: boolean; + createdAt: Date | string; + updatedAt: Date | string; + parentId?: string; + userId: string; +} + +export interface FolderWithRelations extends Folder { + documents: Array<{ + id: string; + title: string; + icon?: string; + updatedAt: string; + }>; + children: FolderWithRelations[]; + _count: { + documents: number; + children: number; + }; +} + +// Folder state +export interface FolderState { + folders: FolderWithRelations[]; + currentFolder: FolderWithRelations | null; + isLoading: boolean; +} + +// Folder actions +export interface FolderActions { + readFolders: (folders: FolderWithRelations[]) => void; + setCurrentFolder: (folder: FolderWithRelations | null) => void; + createFolder: (folder: FolderWithRelations) => void; + updateFolder: (updatedFolder: FolderWithRelations) => void; + deleteFolder: (folderId: string) => void; + setLoading: (isLoading: boolean) => void; +} + diff --git a/nextjs/src/entities/folder/model/validation.ts b/nextjs/src/entities/folder/model/validation.ts new file mode 100644 index 0000000..039a2cf --- /dev/null +++ b/nextjs/src/entities/folder/model/validation.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; + +// Folder validation schemas +export const folderSchema = z.object({ + name: z.string().min(1, 'Name is required').max(100, 'Name is too long'), + icon: z.string().optional(), + color: z.string().optional(), + isArchived: z.boolean().default(false), + parentId: z.string().optional(), +}); + +export type FolderInput = z.infer; + diff --git a/nextjs/src/entities/folder/ui/FolderItem.tsx b/nextjs/src/entities/folder/ui/FolderItem.tsx new file mode 100644 index 0000000..f72c0d9 --- /dev/null +++ b/nextjs/src/entities/folder/ui/FolderItem.tsx @@ -0,0 +1,40 @@ +import { FolderWithRelations } from '../model/types'; +import { cn } from '@/shared/lib/utils'; +import { Folder, Calendar } from 'lucide-react'; +import Link from 'next/link'; + +interface FolderItemProps { + folder: FolderWithRelations; + className?: string; +} + +export const FolderItem = ({ folder, className }: FolderItemProps) => { + const Icon = folder.icon ? + {folder.icon} : + ; + + return ( + +
+ {Icon} +
+
+

{folder.name}

+
+ {folder._count.documents} documents +
+ + {new Date(folder.updatedAt).toLocaleDateString()} +
+
+
+ + ); +}; + diff --git a/nextjs/src/entities/folder/ui/index.ts b/nextjs/src/entities/folder/ui/index.ts new file mode 100644 index 0000000..686b265 --- /dev/null +++ b/nextjs/src/entities/folder/ui/index.ts @@ -0,0 +1,2 @@ +export * from './FolderItem'; + diff --git a/nextjs/src/entities/index.ts b/nextjs/src/entities/index.ts new file mode 100644 index 0000000..08e2f3b --- /dev/null +++ b/nextjs/src/entities/index.ts @@ -0,0 +1,6 @@ +// Export all entities +export * from './document'; +export * from './folder'; +export * from './user'; +export * from './template'; + diff --git a/nextjs/src/entities/template/api/index.ts b/nextjs/src/entities/template/api/index.ts new file mode 100644 index 0000000..e4dab92 --- /dev/null +++ b/nextjs/src/entities/template/api/index.ts @@ -0,0 +1,56 @@ +import type { DatabaseTemplate } from '../model/types'; +import { apiGet, apiPost, apiPut, apiDelete } from '@/shared/api'; + +// Template API functions +export const templateApi = { + // Get all templates + getTemplates: async (): Promise => { + try { + return await apiGet('/api/templates'); + } catch (error) { + console.error('Failed to fetch templates:', error); + throw error; + } + }, + + // Get public templates + getPublicTemplates: async (): Promise => { + try { + return await apiGet('/api/templates?public=true'); + } catch (error) { + console.error('Failed to fetch public templates:', error); + throw error; + } + }, + + // Create a new template + createTemplate: async (newTemplate: Partial): Promise => { + try { + return await apiPost('/api/templates', newTemplate); + } catch (error) { + console.error('Failed to create template:', error); + throw error; + } + }, + + // Update a template + updateTemplate: async (id: string, updateData: Partial): Promise => { + try { + return await apiPut(`/api/templates/${id}`, updateData); + } catch (error) { + console.error('Failed to update template:', error); + throw error; + } + }, + + // Delete a template + deleteTemplate: async (id: string): Promise => { + try { + await apiDelete(`/api/templates/${id}`); + } catch (error) { + console.error('Failed to delete template:', error); + throw error; + } + }, +}; + diff --git a/nextjs/src/entities/template/index.ts b/nextjs/src/entities/template/index.ts new file mode 100644 index 0000000..3062c4b --- /dev/null +++ b/nextjs/src/entities/template/index.ts @@ -0,0 +1,4 @@ +export * from './api'; +export * from './model'; +export * from './ui'; + diff --git a/nextjs/src/entities/template/model/index.ts b/nextjs/src/entities/template/model/index.ts new file mode 100644 index 0000000..5b7b829 --- /dev/null +++ b/nextjs/src/entities/template/model/index.ts @@ -0,0 +1,4 @@ +export * from './types'; +export * from './store'; +export * from './validation'; + diff --git a/nextjs/src/entities/template/model/store.ts b/nextjs/src/entities/template/model/store.ts new file mode 100644 index 0000000..601aa96 --- /dev/null +++ b/nextjs/src/entities/template/model/store.ts @@ -0,0 +1,36 @@ +import { create } from 'zustand'; +import { DatabaseTemplate, TemplateState, TemplateActions } from './types'; + +// Template state management store +export const useTemplateStore = create((set) => ({ + templates: [], + currentTemplate: null, + isLoading: true, + + readTemplates: (templates: DatabaseTemplate[]) => set({ templates }), + + setCurrentTemplate: (template: DatabaseTemplate | null) => set({ currentTemplate: template }), + + createTemplate: (newTemplate: DatabaseTemplate) => set((state) => ({ + templates: [newTemplate, ...state.templates] + })), + + updateTemplate: (updatedTemplate: DatabaseTemplate) => set((state) => ({ + templates: state.templates.map(template => + template.id === updatedTemplate.id ? updatedTemplate : template + ), + currentTemplate: state.currentTemplate?.id === updatedTemplate.id + ? updatedTemplate + : state.currentTemplate + })), + + deleteTemplate: (templateId: string) => set((state) => ({ + templates: state.templates.filter(template => template.id !== templateId), + currentTemplate: state.currentTemplate?.id === templateId + ? null + : state.currentTemplate + })), + + setLoading: (isLoading: boolean) => set({ isLoading }), +})); + diff --git a/nextjs/src/entities/template/model/types.ts b/nextjs/src/entities/template/model/types.ts new file mode 100644 index 0000000..e2648d8 --- /dev/null +++ b/nextjs/src/entities/template/model/types.ts @@ -0,0 +1,49 @@ +// Template entity types +export interface Template { + id: string; + name: string; + description?: string; + icon: React.ReactElement | string; + content: any; +} + +export interface DatabaseTemplate { + id: string; + name: string; + description?: string; + category: string; + title: string; + content: any; + isPublic: boolean; + createdAt: string; + updatedAt: string; + userId: string; +} + +export interface TemplateBrowserProps { + isOpen: boolean; + onClose: () => void; + onSelectTemplate: (template: DatabaseTemplate) => void; +} + +export interface TemplateSelectorProps { + onSelectTemplate: (template: Template | DatabaseTemplate) => void; +} + +// Template state +export interface TemplateState { + templates: DatabaseTemplate[]; + currentTemplate: DatabaseTemplate | null; + isLoading: boolean; +} + +// Template actions +export interface TemplateActions { + readTemplates: (templates: DatabaseTemplate[]) => void; + setCurrentTemplate: (template: DatabaseTemplate | null) => void; + createTemplate: (template: DatabaseTemplate) => void; + updateTemplate: (updatedTemplate: DatabaseTemplate) => void; + deleteTemplate: (templateId: string) => void; + setLoading: (isLoading: boolean) => void; +} + diff --git a/nextjs/src/entities/template/model/validation.ts b/nextjs/src/entities/template/model/validation.ts new file mode 100644 index 0000000..aeda0a1 --- /dev/null +++ b/nextjs/src/entities/template/model/validation.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; + +// Template validation schemas +export const templateSchema = z.object({ + name: z.string().min(1, 'Name is required').max(200, 'Name is too long'), + description: z.string().optional(), + category: z.string().default('General'), + title: z.string().min(1, 'Title is required').max(200, 'Title is too long'), + content: z.any().optional(), + isPublic: z.boolean().default(false), +}); + +export type TemplateInput = z.infer; + diff --git a/nextjs/src/entities/template/ui/index.ts b/nextjs/src/entities/template/ui/index.ts new file mode 100644 index 0000000..2b69f60 --- /dev/null +++ b/nextjs/src/entities/template/ui/index.ts @@ -0,0 +1,6 @@ +// Template UI components would be defined here +// export * from './TemplateCard'; +// export * from './TemplateSelector'; + +export {}; + diff --git a/nextjs/src/entities/todo/api/index.ts b/nextjs/src/entities/todo/api/index.ts new file mode 100644 index 0000000..3c1e547 --- /dev/null +++ b/nextjs/src/entities/todo/api/index.ts @@ -0,0 +1,170 @@ +import type { Todo, CreateTodoData, UpdateTodoData } from '../model/types' + +const TODO_API_URL = process.env.NEXT_PUBLIC_TODO_API_URL || 'http://localhost:3002' + +// API 응답 타입 +interface ApiTodo { + id: number + title: string + description?: string | null + completed: boolean + createdAt: string + updatedAt: string +} + +interface ApiResponse { + success: boolean + data?: T + error?: string +} + +// 백엔드 Todo를 프론트엔드 Todo로 변환 +const mapApiTodoToTodo = (apiTodo: ApiTodo): Todo => { + return { + id: String(apiTodo.id), + title: apiTodo.title, + description: apiTodo.description || undefined, + completed: apiTodo.completed, + createdAt: new Date(apiTodo.createdAt), + updatedAt: new Date(apiTodo.updatedAt), + } +} + +/** + * 모든 TODO 가져오기 + */ +export const fetchTodos = async (): Promise => { + try { + const response = await fetch(`${TODO_API_URL}/api/todos`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const result: ApiResponse = await response.json() + + if (!result.success || !result.data) { + throw new Error(result.error || 'Failed to fetch todos') + } + + return result.data.map(mapApiTodoToTodo) + } catch (error) { + console.error('Error fetching todos:', error) + throw error + } +} + +/** + * TODO 생성 + */ +export const createTodo = async (data: CreateTodoData): Promise => { + try { + const response = await fetch(`${TODO_API_URL}/api/todos`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ + title: data.title, + description: data.description || null, + completed: false, + }), + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const result: ApiResponse = await response.json() + + if (!result.success || !result.data) { + throw new Error(result.error || 'Failed to create todo') + } + + return mapApiTodoToTodo(result.data) + } catch (error) { + console.error('Error creating todo:', error) + throw error + } +} + +/** + * TODO 업데이트 + */ +export const updateTodo = async (id: string, data: UpdateTodoData): Promise => { + try { + const response = await fetch(`${TODO_API_URL}/api/todos/${id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ + title: data.title, + description: data.description, + completed: data.completed, + }), + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const result: ApiResponse = await response.json() + + if (!result.success || !result.data) { + throw new Error(result.error || 'Failed to update todo') + } + + return mapApiTodoToTodo(result.data) + } catch (error) { + console.error('Error updating todo:', error) + throw error + } +} + +/** + * TODO 삭제 + */ +export const deleteTodo = async (id: string): Promise => { + try { + const response = await fetch(`${TODO_API_URL}/api/todos/${id}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const result: ApiResponse = await response.json() + + if (!result.success) { + throw new Error(result.error || 'Failed to delete todo') + } + } catch (error) { + console.error('Error deleting todo:', error) + throw error + } +} + +/** + * Todo API + */ +export const todoApi = { + fetchTodos, + createTodo, + updateTodo, + deleteTodo, +} + diff --git a/nextjs/src/entities/todo/index.ts b/nextjs/src/entities/todo/index.ts new file mode 100644 index 0000000..52b492c --- /dev/null +++ b/nextjs/src/entities/todo/index.ts @@ -0,0 +1,3 @@ +export * from './model' +export * from './api' + diff --git a/nextjs/src/entities/todo/model/index.ts b/nextjs/src/entities/todo/model/index.ts new file mode 100644 index 0000000..e3d98e3 --- /dev/null +++ b/nextjs/src/entities/todo/model/index.ts @@ -0,0 +1,3 @@ +export * from './types' +export * from './store' + diff --git a/nextjs/src/entities/todo/model/store.ts b/nextjs/src/entities/todo/model/store.ts new file mode 100644 index 0000000..69bf818 --- /dev/null +++ b/nextjs/src/entities/todo/model/store.ts @@ -0,0 +1,133 @@ +import { create } from 'zustand' +import { Todo, CreateTodoData, UpdateTodoData, TodoStore, TodoFilter } from './types' +import { fetchTodos, createTodo, updateTodo, deleteTodo } from '../api' + +export const useTodoStore = create()((set, get) => ({ + // State + todos: [], + filter: 'all', + isLoading: false, + error: null, + + // Actions + addTodo: async (data: CreateTodoData) => { + if (!data.title?.trim()) { + console.warn('Todo title is required') + return + } + + try { + const newTodo = await createTodo(data) + set((state) => ({ + todos: [newTodo, ...state.todos] + })) + } catch (error) { + console.error('Failed to create todo:', error) + set({ error: 'Failed to create todo' }) + } + }, + + updateTodo: async (id: string, data: UpdateTodoData) => { + if (!id) { + console.warn('Todo ID is required for update') + return + } + + try { + const updatedTodo = await updateTodo(id, data) + set((state) => ({ + todos: state.todos.map((todo) => + todo.id === id ? updatedTodo : todo + ) + })) + } catch (error) { + console.error('Failed to update todo:', error) + set({ error: 'Failed to update todo' }) + } + }, + + deleteTodo: async (id: string) => { + if (!id) { + console.warn('Todo ID is required for deletion') + return + } + + try { + await deleteTodo(id) + set((state) => ({ + todos: state.todos.filter((todo) => todo.id !== id) + })) + } catch (error) { + console.error('Failed to delete todo:', error) + set({ error: 'Failed to delete todo' }) + } + }, + + toggleTodo: async (id: string) => { + if (!id) { + console.warn('Todo ID is required for toggle') + return + } + + const todo = get().todos.find((t) => t.id === id) + if (!todo) return + + try { + await get().updateTodo(id, { completed: !todo.completed }) + } catch (error) { + console.error('Failed to toggle todo:', error) + } + }, + + loadTodos: async () => { + set({ isLoading: true, error: null }) + try { + const todos = await fetchTodos() + set({ todos, isLoading: false }) + } catch (error) { + console.error('Failed to load todos:', error) + set({ error: 'Failed to load todos', isLoading: false }) + } + }, + + setFilter: (filter: TodoFilter) => { + set({ filter }) + }, + + clearCompleted: async () => { + const completedTodos = get().todos.filter((todo) => todo.completed) + + try { + await Promise.all(completedTodos.map((todo) => deleteTodo(todo.id))) + set((state) => ({ + todos: state.todos.filter((todo) => !todo.completed) + })) + } catch (error) { + console.error('Failed to clear completed todos:', error) + set({ error: 'Failed to clear completed todos' }) + } + }, +})) + +// Selectors +export const useFilteredTodos = () => { + const { todos, filter } = useTodoStore() + + if (!Array.isArray(todos)) { + return [] + } + + return todos.filter((todo) => { + if (!todo || typeof todo !== 'object') { + return false + } + + const matchesFilter = + filter === 'all' || + (filter === 'active' && !todo.completed) || + (filter === 'completed' && todo.completed) + + return matchesFilter + }) +} + diff --git a/nextjs/src/entities/todo/model/types.ts b/nextjs/src/entities/todo/model/types.ts new file mode 100644 index 0000000..6e7a14c --- /dev/null +++ b/nextjs/src/entities/todo/model/types.ts @@ -0,0 +1,42 @@ +export interface Todo { + id: string + title: string + description?: string + completed: boolean + createdAt: Date + updatedAt: Date +} + +export interface CreateTodoData { + title: string + description?: string +} + +export interface UpdateTodoData { + title?: string + description?: string + completed?: boolean +} + +export type TodoFilter = 'all' | 'active' | 'completed' + +// Store types +export interface TodoState { + todos: Todo[] + filter: TodoFilter + isLoading: boolean + error: string | null +} + +export interface TodoActions { + addTodo: (data: CreateTodoData) => Promise + updateTodo: (id: string, data: UpdateTodoData) => Promise + deleteTodo: (id: string) => Promise + toggleTodo: (id: string) => Promise + loadTodos: () => Promise + setFilter: (filter: TodoFilter) => void + clearCompleted: () => Promise +} + +export type TodoStore = TodoState & TodoActions + diff --git a/nextjs/src/entities/todo/ui/TodoItem.tsx b/nextjs/src/entities/todo/ui/TodoItem.tsx new file mode 100644 index 0000000..bcc9d01 --- /dev/null +++ b/nextjs/src/entities/todo/ui/TodoItem.tsx @@ -0,0 +1,132 @@ +'use client' + +import { Todo } from '../model/types' +import { Checkbox } from '@/shared/ui/checkbox' +import { Button } from '@/shared/ui/button' +import { Trash2, Edit2 } from 'lucide-react' +import { cn } from '@/shared/lib/utils' +import { useState } from 'react' +import { Input } from '@/shared/ui/input' + +interface TodoItemProps { + todo: Todo + onToggle: (id: string) => void + onDelete: (id: string) => void + onUpdate: (id: string, title: string, description?: string) => void +} + +export function TodoItem({ todo, onToggle, onDelete, onUpdate }: TodoItemProps) { + const [isEditing, setIsEditing] = useState(false) + const [editTitle, setEditTitle] = useState(todo.title) + const [editDescription, setEditDescription] = useState(todo.description || '') + + const handleSave = () => { + if (editTitle.trim()) { + onUpdate(todo.id, editTitle, editDescription) + setIsEditing(false) + } + } + + const handleCancel = () => { + setEditTitle(todo.title) + setEditDescription(todo.description || '') + setIsEditing(false) + } + + if (isEditing) { + return ( +
+ setEditTitle(e.target.value)} + placeholder="Todo 제목" + className="font-medium" + autoFocus + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + handleSave() + } else if (e.key === 'Escape') { + handleCancel() + } + }} + /> + setEditDescription(e.target.value)} + placeholder="설명 (선택사항)" + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + handleSave() + } else if (e.key === 'Escape') { + handleCancel() + } + }} + /> +
+ + +
+
+ ) + } + + return ( +
+ onToggle(todo.id)} + className="mt-0.5" + /> + +
+
+ {todo.title} +
+ {todo.description && ( +
+ {todo.description} +
+ )} +
+ {new Date(todo.createdAt).toLocaleDateString('ko-KR')} +
+
+ +
+ + +
+
+ ) +} + diff --git a/nextjs/src/entities/todo/ui/TodoList.tsx b/nextjs/src/entities/todo/ui/TodoList.tsx new file mode 100644 index 0000000..6855da1 --- /dev/null +++ b/nextjs/src/entities/todo/ui/TodoList.tsx @@ -0,0 +1,53 @@ +'use client' + +import { TodoItem } from './TodoItem' +import { useTodoStore, useFilteredTodos } from '../model/store' +import { Spinner } from '@/shared/ui/spinner' + +export function TodoList() { + const { updateTodo, deleteTodo, toggleTodo, isLoading, error } = useTodoStore() + const filteredTodos = useFilteredTodos() + + if (isLoading) { + return ( +
+ +
+ ) + } + + if (error) { + return ( +
+ {error} +
+ ) + } + + if (filteredTodos.length === 0) { + return ( +
+ 할 일이 없습니다 +
+ ) + } + + const handleUpdate = async (id: string, title: string, description?: string) => { + await updateTodo(id, { title, description }) + } + + return ( +
+ {filteredTodos.map((todo) => ( + + ))} +
+ ) +} + diff --git a/nextjs/src/entities/todo/ui/index.ts b/nextjs/src/entities/todo/ui/index.ts new file mode 100644 index 0000000..9c1247c --- /dev/null +++ b/nextjs/src/entities/todo/ui/index.ts @@ -0,0 +1,3 @@ +export * from './TodoItem' +export * from './TodoList' + diff --git a/nextjs/src/entities/user/api/index.ts b/nextjs/src/entities/user/api/index.ts new file mode 100644 index 0000000..6cac96f --- /dev/null +++ b/nextjs/src/entities/user/api/index.ts @@ -0,0 +1,53 @@ +import type { User } from '../model/types'; +import { apiGet, apiPost } from '@/shared/api'; + +// User API functions +export const userApi = { + // Get current user + getCurrentUser: async (): Promise => { + try { + return await apiGet('/api/auth/me'); + } catch (error) { + console.error('Failed to fetch user:', error); + throw error; + } + }, + + // Login + login: async (email: string, password: string): Promise<{ user: User; token: string }> => { + try { + return await apiPost<{ user: User; token: string }>('/api/auth/login', { + email, + password, + }); + } catch (error) { + console.error('Failed to login:', error); + throw error; + } + }, + + // Register + register: async (email: string, password: string, name?: string): Promise<{ user: User; token: string }> => { + try { + return await apiPost<{ user: User; token: string }>('/api/auth/register', { + email, + password, + name, + }); + } catch (error) { + console.error('Failed to register:', error); + throw error; + } + }, + + // Logout + logout: async (): Promise => { + try { + await apiPost('/api/auth/logout'); + } catch (error) { + console.error('Failed to logout:', error); + throw error; + } + }, +}; + diff --git a/nextjs/src/entities/user/index.ts b/nextjs/src/entities/user/index.ts new file mode 100644 index 0000000..3062c4b --- /dev/null +++ b/nextjs/src/entities/user/index.ts @@ -0,0 +1,4 @@ +export * from './api'; +export * from './model'; +export * from './ui'; + diff --git a/nextjs/src/entities/user/model/index.ts b/nextjs/src/entities/user/model/index.ts new file mode 100644 index 0000000..5b7b829 --- /dev/null +++ b/nextjs/src/entities/user/model/index.ts @@ -0,0 +1,4 @@ +export * from './types'; +export * from './store'; +export * from './validation'; + diff --git a/nextjs/src/entities/user/model/store.ts b/nextjs/src/entities/user/model/store.ts new file mode 100644 index 0000000..56d978c --- /dev/null +++ b/nextjs/src/entities/user/model/store.ts @@ -0,0 +1,13 @@ +import { create } from 'zustand'; +import { UserState, UserActions, User } from './types'; + +// User state management store +export const useUserStore = create((set) => ({ + user: null, + isLoading: true, + + setUser: (user: User | null) => set({ user }), + + setLoading: (isLoading: boolean) => set({ isLoading }), +})); + diff --git a/nextjs/src/entities/user/model/types.ts b/nextjs/src/entities/user/model/types.ts new file mode 100644 index 0000000..8b7c328 --- /dev/null +++ b/nextjs/src/entities/user/model/types.ts @@ -0,0 +1,27 @@ +// User entity types +export interface User { + id: string; + email: string; + name?: string; + image?: string; +} + +export interface AuthContextType { + user: User | null; + isLoading: boolean; + login: (token: string, user: User) => void; + logout: () => void; +} + +// User state +export interface UserState { + user: User | null; + isLoading: boolean; +} + +// User actions +export interface UserActions { + setUser: (user: User | null) => void; + setLoading: (isLoading: boolean) => void; +} + diff --git a/nextjs/src/entities/user/model/validation.ts b/nextjs/src/entities/user/model/validation.ts new file mode 100644 index 0000000..dee4d27 --- /dev/null +++ b/nextjs/src/entities/user/model/validation.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; + +// User validation schemas +export const userSchema = z.object({ + email: z.string().email('Invalid email address'), + name: z.string().optional(), + image: z.string().url().optional(), +}); + +export type UserInput = z.infer; + diff --git a/nextjs/src/entities/user/ui/index.ts b/nextjs/src/entities/user/ui/index.ts new file mode 100644 index 0000000..c70b0d9 --- /dev/null +++ b/nextjs/src/entities/user/ui/index.ts @@ -0,0 +1,6 @@ +// User UI components would be defined here +// export * from './UserAvatar'; +// export * from './UserProfile'; + +export {}; + diff --git a/nextjs/src/features/document-edit/index.ts b/nextjs/src/features/document-edit/index.ts new file mode 100644 index 0000000..7a689d5 --- /dev/null +++ b/nextjs/src/features/document-edit/index.ts @@ -0,0 +1,3 @@ +export * from './model'; +export * from './ui'; + diff --git a/nextjs/src/features/document-edit/model/index.ts b/nextjs/src/features/document-edit/model/index.ts new file mode 100644 index 0000000..b8d7e86 --- /dev/null +++ b/nextjs/src/features/document-edit/model/index.ts @@ -0,0 +1,9 @@ +export * from './store'; +export * from './use-document-data'; +export * from './use-document-save'; +export * from './use-document-actions'; +export * from './use-document-headings'; +export * from './use-document-utils'; +export * from './use-document-templates'; +export * from './use-sidebar-search'; + diff --git a/nextjs/src/features/document-edit/model/store.ts b/nextjs/src/features/document-edit/model/store.ts new file mode 100644 index 0000000..dd8a451 --- /dev/null +++ b/nextjs/src/features/document-edit/model/store.ts @@ -0,0 +1,70 @@ +import { useCallback } from 'react'; +import { documentApi } from '@/entities/document/api'; +import { useDocumentStore } from '@/entities/document/model/store'; +import type { DocumentWithRelations, DocumentInput } from '@/entities/document/model'; + +// API 호출과 전역 상태 관리를 통합하는 훅 +export const useDocumentEditStore = () => { + const entityStore = useDocumentStore.getState(); + + const readDocuments = useCallback(async () => { + entityStore.setLoading(true); + try { + entityStore.readDocuments(await documentApi.getDocuments()); + } finally { + entityStore.setLoading(false); + } + }, [entityStore]); + + const readDocument = useCallback(async (documentId: string) => { + entityStore.setLoading(true); + try { + const document = await documentApi.getDocument(documentId); + entityStore.setCurrentDocument(document); + return document; + } finally { + entityStore.setLoading(false); + } + }, [entityStore]); + + const createDocument = useCallback(async (newDocument: DocumentInput) => { + entityStore.setLoading(true); + try { + const created = await documentApi.createDocument(newDocument); + entityStore.createDocument(created as DocumentWithRelations); + return created; + } finally { + entityStore.setLoading(false); + } + }, [entityStore]); + + const updateDocument = useCallback(async (documentId: string, updateData: Partial) => { + entityStore.setLoading(true); + try { + const updated = await documentApi.updateDocument(documentId, updateData); + entityStore.updateDocument(updated as DocumentWithRelations); + return updated; + } finally { + entityStore.setLoading(false); + } + }, [entityStore]); + + const deleteDocument = useCallback(async (documentId: string) => { + entityStore.setLoading(true); + try { + await documentApi.deleteDocument(documentId); + entityStore.deleteDocument(documentId); + } finally { + entityStore.setLoading(false); + } + }, [entityStore]); + + return { + readDocuments, + readDocument, + createDocument, + updateDocument, + deleteDocument, + }; +}; + diff --git a/nextjs/src/features/document-edit/model/use-document-actions.tsx b/nextjs/src/features/document-edit/model/use-document-actions.tsx new file mode 100644 index 0000000..d94749d --- /dev/null +++ b/nextjs/src/features/document-edit/model/use-document-actions.tsx @@ -0,0 +1,75 @@ +import { useState, useCallback } from "react"; +import { useRouter } from "next/navigation"; +import { documentApi } from "@/entities/document/api"; + +interface UseDocumentActionsProps { + documentId: string; + onPublishChange?: (isPublished: boolean) => void; +} + +/** + * 문서 액션 (공유, 삭제 등) + */ +export const useDocumentActions = ({ + documentId, + onPublishChange +}: UseDocumentActionsProps) => { + const router = useRouter(); + const [isDeleting, setIsDeleting] = useState(false); + + // Delete document + const deleteDocument = useCallback(async () => { + if (!confirm("Are you sure you want to delete this document?")) return; + + try { + setIsDeleting(true); + await documentApi.deleteDocument(documentId); + router.push("/documents"); + } catch (error) { + console.error("Error deleting document:", error); + } finally { + setIsDeleting(false); + } + }, [documentId, router]); + + // Share document + const shareDocument = useCallback(async () => { + try { + await documentApi.updateDocument(documentId, { + isPublished: true, + }); + + const shareUrl = `${window.location.origin}/share/${documentId}`; + + alert('문서가 공개되었습니다!\n\n공유 링크:\n' + shareUrl); + + onPublishChange?.(true); + } catch (error) { + console.error('Error sharing document:', error); + alert('문서 공유에 실패했습니다'); + } + }, [documentId, onPublishChange]); + + // Unshare document + const unshareDocument = useCallback(async () => { + try { + await documentApi.updateDocument(documentId, { + isPublished: false, + }); + + alert('Document is no longer shared!'); + onPublishChange?.(false); + } catch (error) { + console.error('Error unsharing document:', error); + alert('Failed to unshare document'); + } + }, [documentId, onPublishChange]); + + return { + deleteDocument, + shareDocument, + unshareDocument, + isDeleting, + }; +}; + diff --git a/nextjs/src/features/document-edit/model/use-document-data.tsx b/nextjs/src/features/document-edit/model/use-document-data.tsx new file mode 100644 index 0000000..04f5611 --- /dev/null +++ b/nextjs/src/features/document-edit/model/use-document-data.tsx @@ -0,0 +1,98 @@ +import { useState, useEffect, useCallback } from "react"; +import { useRouter } from "next/navigation"; +import { documentApi } from "@/entities/document/api"; +import type { DocumentWithRelations, DocumentListItem } from "@/entities/document/model"; + +interface UseDocumentDataProps { + documentId: string; +} + +/** + * 문서 데이터 조회 및 기본 상태 관리 + */ +export const useDocumentData = ({ documentId }: UseDocumentDataProps) => { + const router = useRouter(); + const [document, setDocument] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [title, setTitle] = useState(""); + const [content, setContent] = useState(null); + const [availableDocuments, setAvailableDocuments] = useState([]); + + // Fetch document + const fetchDocument = useCallback(async () => { + try { + const data = await documentApi.getDocument(documentId); + setDocument(data); + setTitle(data.title); + setContent(data.content || { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Start writing...', + }, + ], + }, + ], + }); + } catch (error: any) { + console.error("Error fetching document:", error); + if (error.message?.includes('404')) { + router.push("/documents"); + } + } finally { + setIsLoading(false); + } + }, [documentId, router]); + + // Fetch available documents for linking + const fetchAvailableDocuments = useCallback(async () => { + try { + const documents = await documentApi.getDocuments(); + const filteredDocs = documents + .filter((doc: DocumentWithRelations) => doc.id !== documentId) + .map((doc: DocumentWithRelations) => ({ + id: doc.id, + title: doc.title, + updatedAt: typeof doc.updatedAt === 'string' ? doc.updatedAt : new Date(doc.updatedAt).toISOString() + })); + setAvailableDocuments(filteredDocs); + } catch (error) { + console.error("Error fetching available documents:", error); + } + }, [documentId]); + + // Refresh document data + const refreshDocument = useCallback(() => { + setIsLoading(true); + fetchDocument(); + }, [fetchDocument]); + + // Update local document state + const updateDocument = useCallback((updatedDoc: DocumentWithRelations) => { + setDocument(updatedDoc); + }, []); + + useEffect(() => { + if (documentId) { + fetchDocument(); + fetchAvailableDocuments(); + } + }, [documentId, fetchDocument, fetchAvailableDocuments]); + + return { + document, + isLoading, + title, + setTitle, + content, + setContent, + availableDocuments, + refreshDocument, + updateDocument, + }; +}; + diff --git a/nextjs/src/features/document-edit/model/use-document-headings.tsx b/nextjs/src/features/document-edit/model/use-document-headings.tsx new file mode 100644 index 0000000..787c363 --- /dev/null +++ b/nextjs/src/features/document-edit/model/use-document-headings.tsx @@ -0,0 +1,219 @@ +import { useState, useEffect } from 'react'; +import type { HeadingItem } from '@/shared/types'; + +export const useDocumentHeadings = (content: any) => { + const [headingInstances] = useState>(new Map()); + const [headings, setHeadings] = useState([]); + const [treeHeadings, setTreeHeadings] = useState([]); + const [activeHeading, setActiveHeading] = useState(""); + + // Convert flat headings to tree structure + const buildHeadingTree = (flatHeadings: HeadingItem[]): HeadingItem[] => { + if (flatHeadings.length === 0) return []; + + const tree: HeadingItem[] = []; + const stack: HeadingItem[] = []; + + flatHeadings.forEach(heading => { + const newHeading = { ...heading, children: [], isExpanded: true }; + + // Find the correct parent by looking at the stack + while (stack.length > 0 && stack[stack.length - 1].level >= heading.level) { + stack.pop(); + } + + if (stack.length === 0) { + // This is a root level heading + tree.push(newHeading); + } else { + // This is a child of the last heading in stack + const parent = stack[stack.length - 1]; + if (!parent.children) parent.children = []; + parent.children.push(newHeading); + } + + stack.push(newHeading); + }); + + return tree; + }; + + // Extract headings from content + useEffect(() => { + if (!content?.content) return; + + const extractHeadings = (node: any, level = 0): HeadingItem[] => { + if (!node) return []; + + let result: HeadingItem[] = []; + + if (node.type === 'heading' && node.attrs?.level) { + const text = node.content?.map((c: any) => c.text || '').join('') || ''; + // Keep alphanumeric characters (including Korean), spaces become hyphens + const id = text.toLowerCase() + .replace(/\s+/g, '-') // Replace spaces with hyphens + .replace(/[^a-z0-9\u3131-\u3163\uac00-\ud7a3-]+/g, '') // Keep only letters, numbers, Korean chars, and hyphens + .replace(/-+/g, '-') // Replace multiple hyphens with single hyphen + .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens + result.push({ + id, + text, + level: node.attrs.level + }); + } + + if (node.content) { + node.content.forEach((child: any) => { + result.push(...extractHeadings(child, level)); + }); + } + + return result; + }; + + const extractedHeadings = extractHeadings(content); + + // Ensure unique IDs by adding a counter suffix if duplicates exist + const idCounts = new Map(); + const uniqueHeadings = extractedHeadings.map((heading, index) => { + const baseId = heading.id || 'heading'; + + // Count how many times we've seen this base ID + const count = idCounts.get(baseId) || 0; + idCounts.set(baseId, count + 1); + + // If this is the first occurrence, use the base ID + // Otherwise, append a suffix + const uniqueId = count === 0 ? baseId : `${baseId}-${count}`; + + return { + ...heading, + id: uniqueId, + _index: index // Add sequential index for reference + }; + }); + + setHeadings(uniqueHeadings); + setTreeHeadings(buildHeadingTree(uniqueHeadings)); + }, [content]); + + // Track active heading based on scroll position + useEffect(() => { + const handleScroll = () => { + const headingElements = headings.map(h => { + const element = document.querySelector(`[data-heading-id="${h.id}"]`); + return { ...h, element: element as HTMLElement }; + }).filter(h => h.element); + + let currentHeading = ""; + const scrollPosition = window.scrollY + 100; + + for (let i = headingElements.length - 1; i >= 0; i--) { + const heading = headingElements[i]; + if (heading.element && heading.element.offsetTop <= scrollPosition) { + currentHeading = heading.id; + break; + } + } + + setActiveHeading(currentHeading); + }; + + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, [headings]); + + const toggleHeadingExpansion = (headingId: string) => { + const updateHeadingExpansion = (headings: HeadingItem[]): HeadingItem[] => { + return headings.map(heading => { + if (heading.id === headingId) { + return { ...heading, isExpanded: !heading.isExpanded }; + } + if (heading.children) { + return { ...heading, children: updateHeadingExpansion(heading.children) }; + } + return heading; + }); + }; + + setTreeHeadings(updateHeadingExpansion(treeHeadings)); + }; + + const scrollToHeading = (headingId: string) => { + const selector1 = `[data-heading-id="${headingId}"]`; + const allMatches = document.querySelectorAll(selector1); + + // Find the position of this heading in the tree structure + let targetIndex = 0; + + const findHeadingInTree = (items: HeadingItem[], targetId: string, path: number[] = []): number[] | null => { + for (let i = 0; i < items.length; i++) { + const currentPath = [...path, i]; + if (items[i].id === targetId) { + return currentPath; + } + if (items[i].children) { + const result = findHeadingInTree(items[i].children!, targetId, currentPath); + if (result) return result; + } + } + return null; + }; + + const targetPath = findHeadingInTree(treeHeadings, headingId); + + if (targetPath && targetPath.length > 0) { + // Count how many headings with the same ID appear before this position + let count = 0; + + const countBefore = (items: HeadingItem[], targetId: string, targetPath: number[], currentPath: number[] = []): number => { + let count = 0; + for (let i = 0; i < items.length; i++) { + const path = [...currentPath, i]; + + // Check if this is before the target path + if (items[i].id === targetId) { + let isBefore = true; + for (let j = 0; j < Math.min(path.length, targetPath.length); j++) { + if (path[j] > targetPath[j]) { + isBefore = false; + break; + } else if (path[j] < targetPath[j]) { + break; + } + } + if (isBefore && JSON.stringify(path) !== JSON.stringify(targetPath)) { + count++; + } + } + + if (items[i].children) { + count += countBefore(items[i].children!, targetId, targetPath, path); + } + } + return count; + }; + + targetIndex = countBefore(treeHeadings, headingId, targetPath); + } + + // Get the element at the calculated index + if (allMatches.length > 0 && targetIndex < allMatches.length) { + const element = allMatches[targetIndex] as HTMLElement; + element.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } else if (allMatches.length > 0) { + // Fallback to first match + const element = allMatches[0] as HTMLElement; + element.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }; + + return { + headings, + treeHeadings, + activeHeading, + toggleHeadingExpansion, + scrollToHeading, + }; +}; + diff --git a/nextjs/src/features/document-edit/model/use-document-save.tsx b/nextjs/src/features/document-edit/model/use-document-save.tsx new file mode 100644 index 0000000..fc74b88 --- /dev/null +++ b/nextjs/src/features/document-edit/model/use-document-save.tsx @@ -0,0 +1,78 @@ +import { useState, useEffect, useCallback } from "react"; +import { documentApi } from "@/entities/document/api"; +import type { DocumentWithRelations } from "@/entities/document/model"; + +interface UseDocumentSaveProps { + documentId: string; + title: string; + content: any; + onSaveSuccess?: (document: DocumentWithRelations) => void; +} + +/** + * 문서 저장 로직 (수동 저장, 자동 저장) + */ +export const useDocumentSave = ({ + documentId, + title, + content, + onSaveSuccess +}: UseDocumentSaveProps) => { + const [isSaving, setIsSaving] = useState(false); + const [isAutoSaving, setIsAutoSaving] = useState(false); + const [lastSaved, setLastSaved] = useState(null); + + // Save document + const saveDocument = useCallback(async () => { + try { + setIsSaving(true); + const updatedDocument = await documentApi.updateDocument(documentId, { + title: title || "Untitled", + content: content, + }); + setLastSaved(new Date()); + onSaveSuccess?.(updatedDocument as DocumentWithRelations); + } catch (error) { + console.error("Error saving document:", error); + } finally { + setIsSaving(false); + } + }, [documentId, title, content, onSaveSuccess]); + + // Auto-save function + const autoSave = useCallback(async () => { + if (!title.trim() && !content) return; + + try { + setIsAutoSaving(true); + await documentApi.updateDocument(documentId, { + title: title || "Untitled", + content: content, + }); + setLastSaved(new Date()); + } catch (error) { + console.error("Error auto-saving document:", error); + } finally { + setIsAutoSaving(false); + } + }, [documentId, title, content]); + + // Auto-save effect + useEffect(() => { + if (!title.trim() && !content) return; + + const timeoutId = setTimeout(() => { + autoSave(); + }, 2000); // Auto-save after 2 seconds of inactivity + + return () => clearTimeout(timeoutId); + }, [title, content, autoSave]); + + return { + saveDocument, + isSaving, + isAutoSaving, + lastSaved, + }; +}; + diff --git a/nextjs/src/features/document-edit/model/use-document-templates.tsx b/nextjs/src/features/document-edit/model/use-document-templates.tsx new file mode 100644 index 0000000..35c0d7c --- /dev/null +++ b/nextjs/src/features/document-edit/model/use-document-templates.tsx @@ -0,0 +1,40 @@ +import { useCallback } from "react"; +import { apiPost } from "@/shared/lib/api-client"; + +interface UseDocumentTemplatesProps { + onApply?: (content: any, title?: string) => void; +} + +/** + * 템플릿 생성 및 적용 + */ +export const useDocumentTemplates = ({ onApply }: UseDocumentTemplatesProps = {}) => { + // Create template from document + const createTemplate = useCallback(async (templateData: any) => { + try { + const result = await apiPost('/api/templates', templateData); + console.log('Template created successfully:', result); + + alert(`Template "${templateData.name}" created successfully!`); + + } catch (error) { + console.error('Error creating template:', error); + throw error; + } + }, []); + + // Apply template to document + const applyTemplate = useCallback((template: any) => { + if (template.content) { + const title = template.title || undefined; + onApply?.(template.content, title); + alert(`Template "${template.name}" applied successfully!`); + } + }, [onApply]); + + return { + createTemplate, + applyTemplate, + }; +}; + diff --git a/nextjs/src/features/document-edit/model/use-document-utils.tsx b/nextjs/src/features/document-edit/model/use-document-utils.tsx new file mode 100644 index 0000000..caf7266 --- /dev/null +++ b/nextjs/src/features/document-edit/model/use-document-utils.tsx @@ -0,0 +1,37 @@ +import { useCallback } from "react"; + +/** + * 문서 관련 유틸리티 함수 + */ +export const useDocumentUtils = () => { + // Calculate word count + const getWordCount = useCallback((content: any): number => { + if (!content?.content) return 0; + + const extractText = (node: any): string => { + if (node.type === 'text') return node.text || ''; + if (node.content) return node.content.map(extractText).join(' '); + return ''; + }; + + const text = content.content.map(extractText).join(' '); + return text.trim().split(/\s+/).filter((word: string) => word.length > 0).length; + }, []); + + // Format date helper + const formatDate = useCallback((date: Date): string => { + const now = new Date(); + const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1000 * 60)); + + if (diffInMinutes < 1) return "Just now"; + if (diffInMinutes < 60) return `${diffInMinutes}m ago`; + if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)}h ago`; + return date.toLocaleDateString(); + }, []); + + return { + getWordCount, + formatDate, + }; +}; + diff --git a/nextjs/src/features/document-edit/model/use-sidebar-search.tsx b/nextjs/src/features/document-edit/model/use-sidebar-search.tsx new file mode 100644 index 0000000..0decf4e --- /dev/null +++ b/nextjs/src/features/document-edit/model/use-sidebar-search.tsx @@ -0,0 +1,204 @@ +import { useState } from 'react'; + +export interface SearchResult { + type: string; + text: string; + id?: string; + path?: string; +} + +export const useSidebarSearch = (content: any) => { + const [searchQuery, setSearchQuery] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [showSearchResults, setShowSearchResults] = useState(false); + + // Search function to extract text from content + const extractTextFromNode = (node: any, path: string = ''): SearchResult[] => { + if (!node) return []; + + let results: SearchResult[] = []; + + // Extract text from various node types + if (node.content) { + const text = node.content.map((c: any) => c.text || '').join(''); + if (text.trim()) { + let id; + if (node.type === 'heading' && node.attrs?.level) { + // Keep alphanumeric characters (including Korean), spaces become hyphens + const headingText = text.toLowerCase() + .replace(/\s+/g, '-') // Replace spaces with hyphens + .replace(/[^a-z0-9\u3131-\u3163\uac00-\ud7a3-]+/g, '') // Keep only letters, numbers, Korean chars, and hyphens + .replace(/-+/g, '-') // Replace multiple hyphens with single hyphen + .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens + id = headingText; + } + results.push({ + text, + type: node.type, + id, + path + }); + } + } + + // Recursively process children + if (node.content && Array.isArray(node.content)) { + node.content.forEach((child: any, index: number) => { + const childPath = path ? `${path}.${node.type}[${index}]` : `${node.type}[${index}]`; + results.push(...extractTextFromNode(child, childPath)); + }); + } + + return results; + }; + + // Handle search + const handleSearch = () => { + if (!searchQuery.trim()) { + setSearchResults([]); + setShowSearchResults(false); + return; + } + + if (!content?.content) { + setSearchResults([]); + setShowSearchResults(false); + return; + } + + const query = searchQuery.toLowerCase(); + const extractedTexts = extractTextFromNode(content); + + // Count occurrences for unique IDs (same as document-overview) + const idCounts = new Map(); + const matches = extractedTexts + .filter(item => item.text.toLowerCase().includes(query)) + .map(item => { + // Ensure unique IDs for search results + if (item.id) { + const count = idCounts.get(item.id) || 0; + idCounts.set(item.id, count + 1); + const uniqueId = count === 0 ? item.id : `${item.id}-${count}`; + return { + ...item, + id: uniqueId + }; + } + return item; + }); + + setSearchResults(matches); + setShowSearchResults(true); + }; + + // Handle keyboard events + const handleSearchKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleSearch(); + } + }; + + // Clear search + const handleClearSearch = () => { + setSearchQuery(''); + setSearchResults([]); + setShowSearchResults(false); + }; + + // Navigate to search result (using same algorithm as Document Overview) + const handleNavigateToResult = (result: SearchResult) => { + // Try to find using data-heading-id attribute first (for headings) + if (result.id) { + const element = document.querySelector(`[data-heading-id="${result.id}"]`); + if (element) { + scrollAndHighlight(element as HTMLElement); + return; + } + + // Also check data-node-view-wrapper which wraps the actual heading + const proseMirror = document.querySelector('.ProseMirror'); + if (proseMirror) { + const allHeadings = proseMirror.querySelectorAll('h1, h2, h3, h4, h5, h6'); + for (const heading of Array.from(allHeadings)) { + const wrapper = heading.closest('[data-node-view-wrapper]'); + if (wrapper) { + const wrapperId = wrapper.getAttribute('data-heading-id'); + if (wrapperId === result.id) { + scrollAndHighlight(wrapper as HTMLElement); + return; + } + } + + // Also check text-based matching with same ID logic + const headingText = heading.textContent?.toLowerCase() + .replace(/\s+/g, '-') + .replace(/[^a-z0-9\u3131-\u3163\uac00-\ud7a3-]+/g, '') + .replace(/-+/g, '-') + .replace(/^-|-$/g, '') || ''; + if (headingText === result.id) { + scrollAndHighlight(heading as HTMLElement); + return; + } + } + } + } + + // Try to find in ProseMirror editor content by text + const proseMirror = document.querySelector('.ProseMirror'); + + if (proseMirror) { + const allElements = proseMirror.querySelectorAll('h1, h2, h3, h4, h5, h6, p, li, blockquote'); + + // Try to find by text content match + for (const el of Array.from(allElements)) { + const elementText = el.textContent || ''; + if (elementText.trim() === result.text.trim()) { + scrollAndHighlight(el as HTMLElement); + return; + } + } + } + }; + + const scrollAndHighlight = (element: HTMLElement) => { + // Simple and reliable scroll (same as Document Overview) + element.scrollIntoView({ behavior: 'smooth', block: 'start' }); + + // Add highlight effect + element.classList.add('bg-yellow-200', 'dark:bg-yellow-900', 'ring-2', 'ring-yellow-400', 'transition-all', 'duration-300', 'rounded'); + + setTimeout(() => { + element.classList.remove('bg-yellow-200', 'dark:bg-yellow-900', 'ring-2', 'ring-yellow-400'); + }, 3000); + }; + + // Highlight matching text + const highlightMatch = (text: string, query: string) => { + if (!query.trim()) return text; + + const regex = new RegExp(`(${query})`, 'gi'); + const parts = text.split(regex); + + return parts.map((part, index) => + regex.test(part) ? ( + {part} + ) : ( + {part} + ) + ); + }; + + return { + searchQuery, + setSearchQuery, + searchResults, + showSearchResults, + setShowSearchResults, + handleSearch, + handleSearchKeyDown, + handleClearSearch, + handleNavigateToResult, + highlightMatch, + }; +}; + diff --git a/nextjs/src/features/document-edit/ui/index.ts b/nextjs/src/features/document-edit/ui/index.ts new file mode 100644 index 0000000..82f6347 --- /dev/null +++ b/nextjs/src/features/document-edit/ui/index.ts @@ -0,0 +1,5 @@ +// Document edit UI components +// export * from './DocumentEditor'; + +export {}; + diff --git a/nextjs/src/features/document-management/index.ts b/nextjs/src/features/document-management/index.ts new file mode 100644 index 0000000..7a689d5 --- /dev/null +++ b/nextjs/src/features/document-management/index.ts @@ -0,0 +1,3 @@ +export * from './model'; +export * from './ui'; + diff --git a/nextjs/src/features/document-management/model/index.ts b/nextjs/src/features/document-management/model/index.ts new file mode 100644 index 0000000..84908d3 --- /dev/null +++ b/nextjs/src/features/document-management/model/index.ts @@ -0,0 +1,2 @@ +export * from './store'; + diff --git a/nextjs/src/features/document-management/model/store.ts b/nextjs/src/features/document-management/model/store.ts new file mode 100644 index 0000000..998c662 --- /dev/null +++ b/nextjs/src/features/document-management/model/store.ts @@ -0,0 +1,153 @@ +import { useCallback, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { documentApi } from '@/entities/document/api'; +import { useDocumentStore } from '@/entities/document/model/store'; +import type { DocumentWithRelations, DocumentInput } from '@/entities/document/model'; + +// Document management feature store +export const useDocumentManagementStore = () => { + const entityStore = useDocumentStore.getState(); + const router = useRouter(); + const [isCreating, setIsCreating] = useState(false); + + const fetchDocuments = useCallback(async () => { + entityStore.setLoading(true); + try { + const data = await documentApi.getDocuments(); + entityStore.readDocuments(data); + } catch (error) { + console.error("Error fetching documents:", error); + } finally { + entityStore.setLoading(false); + } + }, [entityStore]); + + const fetchDocumentsInFolder = useCallback(async (folderId: string) => { + try { + const data = await documentApi.getDocuments(); + // Filter by folder and code files + return data.filter((doc: any) => + doc.folderId === folderId && doc.type !== 'CODE_FILE' + ); + } catch (error) { + console.error("Error fetching documents in folder:", error); + return []; + } + }, []); + + const createDocument = useCallback(async (folderId?: string | null) => { + if (isCreating) { + console.log("Document creation already in progress"); + return; + } + + try { + setIsCreating(true); + const newDocument: DocumentInput = { + title: "Untitled", + type: 'PAGE', + isPublished: false, + isArchived: false, + folderId: folderId || undefined, + }; + const created = await documentApi.createDocument(newDocument); + await fetchDocuments(); + router.push(`/documents/${created.id}`); + return created; + } catch (error) { + console.error("Error creating document:", error); + throw error; + } finally { + setIsCreating(false); + } + }, [router, fetchDocuments, isCreating]); + + const deleteDocumentFromFolder = useCallback(async (documentId: string) => { + try { + await documentApi.deleteDocument(documentId); + await fetchDocuments(); + } catch (error) { + console.error("Error deleting document:", error); + throw error; + } + }, [fetchDocuments]); + + const updateDocumentInStore = useCallback((documentId: string, updates: Partial) => { + entityStore.updateDocument({ + ...entityStore.documents.find(d => d.id === documentId)!, + ...updates + } as DocumentWithRelations); + }, [entityStore]); + + const searchDocuments = useCallback(async (query: string) => { + entityStore.setLoading(true); + try { + const documents = await documentApi.getDocuments(); + // Filter documents by query + const filtered = documents.filter(doc => + doc.title.toLowerCase().includes(query.toLowerCase()) + ); + entityStore.readDocuments(filtered); + } finally { + entityStore.setLoading(false); + } + }, [entityStore]); + + const archiveDocument = useCallback(async (documentId: string) => { + entityStore.setLoading(true); + try { + await documentApi.updateDocument(documentId, { isArchived: true }); + const documents = await documentApi.getDocuments(); + entityStore.readDocuments(documents); + } finally { + entityStore.setLoading(false); + } + }, [entityStore]); + + const restoreDocument = useCallback(async (documentId: string) => { + entityStore.setLoading(true); + try { + await documentApi.updateDocument(documentId, { isArchived: false }); + const documents = await documentApi.getDocuments(); + entityStore.readDocuments(documents); + } finally { + entityStore.setLoading(false); + } + }, [entityStore]); + + const duplicateDocument = useCallback(async (documentId: string) => { + entityStore.setLoading(true); + try { + const original = await documentApi.getDocument(documentId); + const newDocument: DocumentInput = { + title: `${original.title} (Copy)`, + content: original.content, + icon: original.icon, + cover: original.cover, + type: original.type, + isPublished: false, + isArchived: false, + folderId: original.folderId, + }; + const created = await documentApi.createDocument(newDocument); + entityStore.createDocument(created as DocumentWithRelations); + return created; + } finally { + entityStore.setLoading(false); + } + }, [entityStore]); + + return { + fetchDocuments, + fetchDocumentsInFolder, + createDocument, + deleteDocumentFromFolder, + updateDocumentInStore, + searchDocuments, + archiveDocument, + restoreDocument, + duplicateDocument, + isCreating, + }; +}; + diff --git a/nextjs/src/features/document-management/ui/index.ts b/nextjs/src/features/document-management/ui/index.ts new file mode 100644 index 0000000..2ad3a6b --- /dev/null +++ b/nextjs/src/features/document-management/ui/index.ts @@ -0,0 +1,6 @@ +// Document management UI components +// export * from './DocumentList'; +// export * from './DocumentSearch'; + +export {}; + diff --git a/nextjs/src/features/folder-management/index.ts b/nextjs/src/features/folder-management/index.ts new file mode 100644 index 0000000..7a689d5 --- /dev/null +++ b/nextjs/src/features/folder-management/index.ts @@ -0,0 +1,3 @@ +export * from './model'; +export * from './ui'; + diff --git a/nextjs/src/features/folder-management/model/index.ts b/nextjs/src/features/folder-management/model/index.ts new file mode 100644 index 0000000..84908d3 --- /dev/null +++ b/nextjs/src/features/folder-management/model/index.ts @@ -0,0 +1,2 @@ +export * from './store'; + diff --git a/nextjs/src/features/folder-management/model/store.ts b/nextjs/src/features/folder-management/model/store.ts new file mode 100644 index 0000000..e6a85c5 --- /dev/null +++ b/nextjs/src/features/folder-management/model/store.ts @@ -0,0 +1,143 @@ +import { useCallback, useState } from 'react'; +import { folderApi } from '@/entities/folder/api'; +import { useFolderStore } from '@/entities/folder/model/store'; +import type { FolderWithRelations, FolderInput } from '@/entities/folder/model'; + +// Folder management feature store +export const useFolderManagementStore = () => { + const entityStore = useFolderStore.getState(); + const [isCreating, setIsCreating] = useState(false); + + const fetchFolders = useCallback(async () => { + entityStore.setLoading(true); + try { + const data = await folderApi.getFolders(); + entityStore.readFolders(data); + } catch (error) { + console.error("Error fetching folders:", error); + } finally { + entityStore.setLoading(false); + } + }, [entityStore]); + + const fetchFolder = useCallback(async (folderId: string) => { + try { + return await folderApi.getFolder(folderId); + } catch (error) { + console.error("Error fetching folder:", error); + return null; + } + }, []); + + const createFolder = useCallback(async (name: string, parentId?: string | null) => { + if (isCreating) { + console.log("Folder creation already in progress, skipping duplicate call"); + return null; + } + + try { + setIsCreating(true); + const newFolder: FolderInput = { + name: name.trim(), + icon: undefined, + color: undefined, + isArchived: false, + parentId: parentId || undefined, + }; + const created = await folderApi.createFolder(newFolder); + await fetchFolders(); + return created; + } catch (error) { + console.error("Error creating folder:", error); + return null; + } finally { + setIsCreating(false); + } + }, [isCreating, fetchFolders]); + + const updateFolder = useCallback(async (folderId: string, name: string) => { + try { + await folderApi.updateFolder(folderId, { name: name.trim() }); + await fetchFolders(); + } catch (error) { + console.error("Error updating folder:", error); + } + }, [fetchFolders]); + + const deleteFolderFromList = useCallback(async (folderId: string) => { + try { + await folderApi.deleteFolder(folderId); + await fetchFolders(); + return true; + } catch (error) { + console.error("Error deleting folder:", error); + return false; + } + }, [fetchFolders]); + + const archiveFolder = useCallback(async (folderId: string) => { + entityStore.setLoading(true); + try { + await folderApi.updateFolder(folderId, { isArchived: true }); + const folders = await folderApi.getFolders(); + entityStore.readFolders(folders); + } finally { + entityStore.setLoading(false); + } + }, [entityStore]); + + const restoreFolder = useCallback(async (folderId: string) => { + entityStore.setLoading(true); + try { + await folderApi.updateFolder(folderId, { isArchived: false }); + const folders = await folderApi.getFolders(); + entityStore.readFolders(folders); + } finally { + entityStore.setLoading(false); + } + }, [entityStore]); + + const moveFolder = useCallback(async (folderId: string, parentId: string | undefined) => { + entityStore.setLoading(true); + try { + await folderApi.updateFolder(folderId, { parentId }); + const folders = await folderApi.getFolders(); + entityStore.readFolders(folders); + } finally { + entityStore.setLoading(false); + } + }, [entityStore]); + + const duplicateFolder = useCallback(async (folderId: string) => { + entityStore.setLoading(true); + try { + const original = await folderApi.getFolder(folderId); + const newFolder: FolderInput = { + name: `${original.name} (Copy)`, + icon: original.icon, + color: original.color, + isArchived: false, + parentId: original.parentId, + }; + const created = await folderApi.createFolder(newFolder); + entityStore.createFolder(created as FolderWithRelations); + return created; + } finally { + entityStore.setLoading(false); + } + }, [entityStore]); + + return { + fetchFolders, + fetchFolder, + createFolder, + updateFolder, + deleteFolderFromList, + archiveFolder, + restoreFolder, + moveFolder, + duplicateFolder, + isCreating, + }; +}; + diff --git a/nextjs/src/features/folder-management/ui/index.ts b/nextjs/src/features/folder-management/ui/index.ts new file mode 100644 index 0000000..736fa00 --- /dev/null +++ b/nextjs/src/features/folder-management/ui/index.ts @@ -0,0 +1,6 @@ +// Folder management UI components +// export * from './FolderTree'; +// export * from './FolderActions'; + +export {}; + diff --git a/nextjs/src/features/index.ts b/nextjs/src/features/index.ts new file mode 100644 index 0000000..aea3315 --- /dev/null +++ b/nextjs/src/features/index.ts @@ -0,0 +1,6 @@ +// Export all features +export * from './document-edit'; +export * from './document-management'; +export * from './folder-management'; +export * from './template-apply'; + diff --git a/nextjs/src/features/template-apply/index.ts b/nextjs/src/features/template-apply/index.ts new file mode 100644 index 0000000..7a689d5 --- /dev/null +++ b/nextjs/src/features/template-apply/index.ts @@ -0,0 +1,3 @@ +export * from './model'; +export * from './ui'; + diff --git a/nextjs/src/features/template-apply/model/index.ts b/nextjs/src/features/template-apply/model/index.ts new file mode 100644 index 0000000..84908d3 --- /dev/null +++ b/nextjs/src/features/template-apply/model/index.ts @@ -0,0 +1,2 @@ +export * from './store'; + diff --git a/nextjs/src/features/template-apply/model/store.ts b/nextjs/src/features/template-apply/model/store.ts new file mode 100644 index 0000000..3c31421 --- /dev/null +++ b/nextjs/src/features/template-apply/model/store.ts @@ -0,0 +1,42 @@ +import { useCallback } from 'react'; +import { templateApi } from '@/entities/template/api'; +import { useTemplateStore } from '@/entities/template/model/store'; +import type { DatabaseTemplate } from '@/entities/template/model'; + +// Template apply feature store +export const useTemplateApplyStore = () => { + const entityStore = useTemplateStore.getState(); + + const applyTemplate = useCallback(async (documentId: string, template: DatabaseTemplate) => { + entityStore.setLoading(true); + try { + // Apply template content to document + await templateApi.updateTemplate(template.id, { + // Template application logic + }); + return template; + } finally { + entityStore.setLoading(false); + } + }, [entityStore]); + + const createTemplateFromDocument = useCallback(async (documentId: string, templateData: Partial) => { + entityStore.setLoading(true); + try { + const template = await templateApi.createTemplate({ + ...templateData, + content: templateData.content || {}, + }); + entityStore.createTemplate(template); + return template; + } finally { + entityStore.setLoading(false); + } + }, [entityStore]); + + return { + applyTemplate, + createTemplateFromDocument, + }; +}; + diff --git a/nextjs/src/features/template-apply/ui/index.ts b/nextjs/src/features/template-apply/ui/index.ts new file mode 100644 index 0000000..b8d9b97 --- /dev/null +++ b/nextjs/src/features/template-apply/ui/index.ts @@ -0,0 +1,6 @@ +// Template apply UI components +// export * from './TemplateBrowser'; +// export * from './TemplateSelector'; + +export {}; + diff --git a/nextjs/src/features/todo-management/index.ts b/nextjs/src/features/todo-management/index.ts new file mode 100644 index 0000000..e963376 --- /dev/null +++ b/nextjs/src/features/todo-management/index.ts @@ -0,0 +1,2 @@ +export * from './ui' + diff --git a/nextjs/src/features/todo-management/ui/TodoCreateForm.tsx b/nextjs/src/features/todo-management/ui/TodoCreateForm.tsx new file mode 100644 index 0000000..d491149 --- /dev/null +++ b/nextjs/src/features/todo-management/ui/TodoCreateForm.tsx @@ -0,0 +1,68 @@ +'use client' + +import { useState } from 'react' +import { Button } from '@/shared/ui/button' +import { Input } from '@/shared/ui/input' +import { Plus } from 'lucide-react' +import { useTodoStore } from '@/entities/todo' + +export function TodoCreateForm() { + const [title, setTitle] = useState('') + const [description, setDescription] = useState('') + const [isExpanded, setIsExpanded] = useState(false) + const { addTodo } = useTodoStore() + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!title.trim()) return + + await addTodo({ + title: title.trim(), + description: description.trim() || undefined, + }) + + setTitle('') + setDescription('') + setIsExpanded(false) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + handleSubmit(e) + } else if (e.key === 'Escape') { + setIsExpanded(false) + setTitle('') + setDescription('') + } + } + + return ( +
+
+ setTitle(e.target.value)} + onFocus={() => setIsExpanded(true)} + onKeyDown={handleKeyDown} + placeholder="새로운 할 일 추가..." + className="flex-1" + /> + +
+ + {isExpanded && ( + setDescription(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="설명 (선택사항)" + /> + )} +
+ ) +} + diff --git a/nextjs/src/features/todo-management/ui/TodoFilter.tsx b/nextjs/src/features/todo-management/ui/TodoFilter.tsx new file mode 100644 index 0000000..f7b70df --- /dev/null +++ b/nextjs/src/features/todo-management/ui/TodoFilter.tsx @@ -0,0 +1,52 @@ +'use client' + +import { Button } from '@/shared/ui/button' +import { useTodoStore } from '@/entities/todo' +import { TodoFilter as TodoFilterType } from '@/entities/todo' + +export function TodoFilter() { + const { filter, setFilter, clearCompleted, todos } = useTodoStore() + + const completedCount = todos.filter(t => t.completed).length + const activeCount = todos.filter(t => !t.completed).length + + return ( +
+
+ + + +
+ + {completedCount > 0 && ( + + )} +
+ ) +} + diff --git a/nextjs/src/features/todo-management/ui/index.ts b/nextjs/src/features/todo-management/ui/index.ts new file mode 100644 index 0000000..aec6bae --- /dev/null +++ b/nextjs/src/features/todo-management/ui/index.ts @@ -0,0 +1,3 @@ +export * from './TodoCreateForm' +export * from './TodoFilter' + diff --git a/nextjs/src/shared/api/client.ts b/nextjs/src/shared/api/client.ts new file mode 100644 index 0000000..34e6b3c --- /dev/null +++ b/nextjs/src/shared/api/client.ts @@ -0,0 +1,65 @@ +// API client with automatic cookie handling (credentials: 'include') + +export const apiFetch = async (url: string, options: RequestInit = {}) => { + return fetch(url, { + ...options, + credentials: 'include', // Automatically include cookies + }) +} + +export const apiGet = async (url: string): Promise => { + const response = await apiFetch(url) + if (!response.ok) { + throw new Error(`API request failed: ${response.statusText}`) + } + return response.json() +} + +export const apiPost = async (url: string, data?: any): Promise => { + const response = await apiFetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: data ? JSON.stringify(data) : undefined, + }) + if (!response.ok) { + throw new Error(`API request failed: ${response.statusText}`) + } + return response.json() +} + +export const apiPut = async (url: string, data?: any): Promise => { + const response = await apiFetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: data ? JSON.stringify(data) : undefined, + }) + if (!response.ok) { + throw new Error(`API request failed: ${response.statusText}`) + } + return response.json() +} + +export const apiDelete = async (url: string): Promise => { + const response = await apiFetch(url, { + method: 'DELETE', + }) + if (!response.ok) { + throw new Error(`API request failed: ${response.statusText}`) + } + return response.json() +} + +export const apiPostFormData = async (url: string, formData: FormData): Promise => { + const response = await apiFetch(url, { + method: 'POST', + body: formData, + }) + if (!response.ok) { + throw new Error(`API request failed: ${response.statusText}`) + } + return response.json() +} diff --git a/nextjs/src/shared/api/index.ts b/nextjs/src/shared/api/index.ts new file mode 100644 index 0000000..83dae76 --- /dev/null +++ b/nextjs/src/shared/api/index.ts @@ -0,0 +1 @@ +export * from './client' diff --git a/nextjs/src/shared/config/s3.ts b/nextjs/src/shared/config/s3.ts new file mode 100644 index 0000000..849aa7f --- /dev/null +++ b/nextjs/src/shared/config/s3.ts @@ -0,0 +1,20 @@ +// S3 설정 +export const S3_CONFIG = { + BUCKET_NAME: process.env.AWS_S3_BUCKET_NAME || 'jooeng', + REGION: process.env.AWS_REGION || 'ap-northeast-2', + ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID || '', + SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY || '', + BUCKET_URL: process.env.AWS_S3_BUCKET_URL || 'https://jooeng.s3.ap-northeast-2.amazonaws.com', +} as const; + +// S3 클라이언트 인스턴스 (서버 사이드에서만 사용) +import { S3Client } from '@aws-sdk/client-s3'; + +export const s3Client = new S3Client({ + region: S3_CONFIG.REGION, + credentials: { + accessKeyId: S3_CONFIG.ACCESS_KEY_ID, + secretAccessKey: S3_CONFIG.SECRET_ACCESS_KEY, + }, +}); + diff --git a/nextjs/src/shared/hooks/index.ts b/nextjs/src/shared/hooks/index.ts new file mode 100644 index 0000000..ebcfd9e --- /dev/null +++ b/nextjs/src/shared/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './use-scroll-top'; +export * from './use-folder-navigation'; diff --git a/nextjs/src/shared/hooks/use-folder-navigation.tsx b/nextjs/src/shared/hooks/use-folder-navigation.tsx new file mode 100644 index 0000000..cce17e7 --- /dev/null +++ b/nextjs/src/shared/hooks/use-folder-navigation.tsx @@ -0,0 +1,42 @@ +import { useState, useCallback } from "react"; +import { FolderWithRelations } from "@/entities/folder/model"; + +export const useFolderNavigation = () => { + const [currentFolder, setCurrentFolder] = useState(null); + const [folderHistory, setFolderHistory] = useState([]); + + const navigateToFolder = useCallback((folder: FolderWithRelations | null) => { + if (folder) { + setFolderHistory(prev => [...prev, folder.id]); + } else { + setFolderHistory([]); + } + setCurrentFolder(folder); + }, []); + + const goBack = useCallback(() => { + if (currentFolder?.parentId) { + // Will navigate to parent, component will handle fetching + return currentFolder.parentId; + } else { + // Go to root + setCurrentFolder(null); + setFolderHistory([]); + return null; + } + }, [currentFolder]); + + const goToRoot = useCallback(() => { + setCurrentFolder(null); + setFolderHistory([]); + }, []); + + return { + currentFolder, + folderHistory, + navigateToFolder, + goBack, + goToRoot, + }; +}; + diff --git a/nextjs/src/shared/hooks/use-scroll-top.tsx b/nextjs/src/shared/hooks/use-scroll-top.tsx new file mode 100644 index 0000000..3158ca4 --- /dev/null +++ b/nextjs/src/shared/hooks/use-scroll-top.tsx @@ -0,0 +1,21 @@ +import { useState, useEffect } from "react"; + +export const useScrollTop = (threshold = 10) => { + const [scrolled,setScrolled] = useState(false); + + useEffect(() => { + const handleScroll = () => { + if(window.scrollY > threshold){ + setScrolled(true); + } + else{ + setScrolled(false); + } + }; + + window.addEventListener("scroll",handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, [threshold]); + + return scrolled; +} \ No newline at end of file diff --git a/nextjs/src/shared/index.ts b/nextjs/src/shared/index.ts new file mode 100644 index 0000000..a41686e --- /dev/null +++ b/nextjs/src/shared/index.ts @@ -0,0 +1,6 @@ +// Shared layer barrel exports +export * from './ui' +export * from './lib' +export * from './api' +export * from './types' + diff --git a/nextjs/src/shared/lib/api-client.ts b/nextjs/src/shared/lib/api-client.ts new file mode 100644 index 0000000..d2c32a2 --- /dev/null +++ b/nextjs/src/shared/lib/api-client.ts @@ -0,0 +1,66 @@ +// API client with automatic cookie handling (credentials: 'include') + +export const apiFetch = async (url: string, options: RequestInit = {}) => { + return fetch(url, { + ...options, + credentials: 'include', // Automatically include cookies + }) +} + +export const apiGet = async (url: string): Promise => { + const response = await apiFetch(url) + if (!response.ok) { + throw new Error(`API request failed: ${response.statusText}`) + } + return response.json() +} + +export const apiPost = async (url: string, data?: any): Promise => { + const response = await apiFetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: data ? JSON.stringify(data) : undefined, + }) + if (!response.ok) { + throw new Error(`API request failed: ${response.statusText}`) + } + return response.json() +} + +export const apiPut = async (url: string, data?: any): Promise => { + const response = await apiFetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: data ? JSON.stringify(data) : undefined, + }) + if (!response.ok) { + throw new Error(`API request failed: ${response.statusText}`) + } + return response.json() +} + +export const apiDelete = async (url: string): Promise => { + const response = await apiFetch(url, { + method: 'DELETE', + }) + if (!response.ok) { + throw new Error(`API request failed: ${response.statusText}`) + } + return response.json() +} + +export const apiPostFormData = async (url: string, formData: FormData): Promise => { + const response = await apiFetch(url, { + method: 'POST', + body: formData, + }) + if (!response.ok) { + throw new Error(`API request failed: ${response.statusText}`) + } + return response.json() +} + diff --git a/nextjs/src/shared/lib/auth.ts b/nextjs/src/shared/lib/auth.ts new file mode 100644 index 0000000..743a5ea --- /dev/null +++ b/nextjs/src/shared/lib/auth.ts @@ -0,0 +1,59 @@ +import jwt from 'jsonwebtoken' +import bcrypt from 'bcryptjs' +import { db } from './db' + +const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production' + +export interface JWTPayload { + userId: string + email: string +} + +export async function hashPassword(password: string): Promise { + return bcrypt.hash(password, 12) +} + +export async function verifyPassword(password: string, hashedPassword: string): Promise { + return bcrypt.compare(password, hashedPassword) +} + +export function generateToken(payload: JWTPayload): string { + return jwt.sign(payload, JWT_SECRET, { expiresIn: '7d' }) +} + +export function verifyToken(token: string): JWTPayload | null { + try { + return jwt.verify(token, JWT_SECRET) as JWTPayload + } catch { + return null + } +} + +export async function createUser(email: string, password: string, name?: string) { + const hashedPassword = await hashPassword(password) + + return db.user.create({ + data: { + email, + password: hashedPassword, + name, + }, + }) +} + +export async function authenticateUser(email: string, password: string) { + const user = await db.user.findUnique({ + where: { email }, + }) + + if (!user) { + return null + } + + const isValid = await verifyPassword(password, user.password) + if (!isValid) { + return null + } + + return user +} diff --git a/nextjs/src/shared/lib/db.ts b/nextjs/src/shared/lib/db.ts new file mode 100644 index 0000000..3f64c49 --- /dev/null +++ b/nextjs/src/shared/lib/db.ts @@ -0,0 +1,18 @@ +import { PrismaClient } from '@prisma/client' + +const globalForPrisma = globalThis as unknown as { + prisma: PrismaClient | undefined +} + +export const db = + globalForPrisma.prisma ?? + new PrismaClient({ + datasources: { + db: { + url: process.env.DATABASE_URL + "?connection_limit=5&pool_timeout=20" + } + }, + log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], + }) + +if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db diff --git a/nextjs/src/shared/lib/index.ts b/nextjs/src/shared/lib/index.ts new file mode 100644 index 0000000..0b1cd4a --- /dev/null +++ b/nextjs/src/shared/lib/index.ts @@ -0,0 +1,2 @@ +export * from './utils' +export * from './s3' diff --git a/nextjs/src/shared/lib/metadata-extractor.ts b/nextjs/src/shared/lib/metadata-extractor.ts new file mode 100644 index 0000000..6ea4f85 --- /dev/null +++ b/nextjs/src/shared/lib/metadata-extractor.ts @@ -0,0 +1,186 @@ +import { parse } from 'node-html-parser'; + +export interface BookmarkMetadata { + title?: string; + description?: string; + image?: string; + url: string; + domain?: string; + author?: string; +} + +function extractMetaContent(doc: any, property: string, name?: string): string { + // Try Open Graph first + let element = doc.querySelector(`meta[property="${property}"]`); + if (element) { + return element.getAttribute('content') || ''; + } + + // Try Twitter Card + if (property.startsWith('og:')) { + const twitterProperty = property.replace('og:', 'twitter:'); + element = doc.querySelector(`meta[name="${twitterProperty}"]`); + if (element) { + return element.getAttribute('content') || ''; + } + } + + // Try standard meta name + if (name) { + element = doc.querySelector(`meta[name="${name}"]`); + if (element) { + return element.getAttribute('content') || ''; + } + } + + return ''; +} + +function extractTitle(doc: any): string { + // Try title tag first + const titleElement = doc.querySelector('title'); + if (titleElement) { + const title = titleElement.textContent?.trim(); + if (title && title.length > 0) { + return title; + } + } + + // Try Open Graph title + const ogTitle = extractMetaContent(doc, 'og:title'); + if (ogTitle) return ogTitle; + + // Try Twitter title + const twitterTitle = extractMetaContent(doc, 'twitter:title'); + if (twitterTitle) return twitterTitle; + + return ''; +} + +function extractDescription(doc: any): string { + // Try Open Graph description + const ogDesc = extractMetaContent(doc, 'og:description'); + if (ogDesc) return ogDesc; + + // Try Twitter description + const twitterDesc = extractMetaContent(doc, 'twitter:description'); + if (twitterDesc) return twitterDesc; + + // Try standard meta description + const metaDesc = extractMetaContent(doc, 'description'); + if (metaDesc) return metaDesc; + + return ''; +} + +function extractImage(doc: any, baseUrl: string): string { + // Try Open Graph image + let image = extractMetaContent(doc, 'og:image'); + if (image) { + // Convert relative URLs to absolute + if (image.startsWith('//')) { + image = 'https:' + image; + } else if (image.startsWith('/')) { + image = new URL(image, baseUrl).toString(); + } + return image; + } + + // Try Twitter image + image = extractMetaContent(doc, 'twitter:image'); + if (image) { + // Convert relative URLs to absolute + if (image.startsWith('//')) { + image = 'https:' + image; + } else if (image.startsWith('/')) { + image = new URL(image, baseUrl).toString(); + } + return image; + } + + return ''; +} + +function extractAuthor(doc: any): string { + // Try Open Graph site_name as author fallback + const siteName = extractMetaContent(doc, 'og:site_name'); + if (siteName) return siteName; + + // Try author meta + const author = extractMetaContent(doc, 'author'); + if (author) return author; + + return ''; +} + +export async function extractBookmarkMetadata(url: string): Promise { + try { + console.log('Extracting metadata for URL:', url); + + // Validate URL + new URL(url); + + // Fetch the webpage + const response = await fetch(url, { + method: 'GET', + headers: { + 'User-Agent': 'Mozilla/5.0 (compatible; Jotion/1.0; +https://jotion.com)', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + }, + }); + + console.log('Response status:', response.status); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const html = await response.text(); + console.log('HTML length:', html.length); + + const doc = parse(html); + + // Extract metadata + const title = extractTitle(doc); + const description = extractDescription(doc); + const image = extractImage(doc, url); + const author = extractAuthor(doc); + + // Extract domain from URL + const domain = new URL(url).hostname; + + const metadata = { + title: title || '', + description: description || '', + image: image || '', + url, + domain, + author: author || '', + }; + + console.log('Extracted metadata:', metadata); + return metadata; + } catch (error) { + console.error('Error extracting bookmark metadata:', error); + + // Return basic info even if metadata extraction fails + const domain = new URL(url).hostname; + return { + title: '', + description: '', + image: '', + url, + domain, + author: '', + }; + } +} + +export function isValidUrl(string: string): boolean { + try { + new URL(string); + return true; + } catch (_) { + return false; + } +} diff --git a/nextjs/src/shared/lib/middleware.ts b/nextjs/src/shared/lib/middleware.ts new file mode 100644 index 0000000..ae83cfc --- /dev/null +++ b/nextjs/src/shared/lib/middleware.ts @@ -0,0 +1,45 @@ +import { NextRequest, NextResponse } from 'next/server' +import { verifyToken } from './auth' + +export function withAuth(handler: (req: NextRequest, userId: string) => Promise) { + return async (req: NextRequest) => { + // Handle CORS preflight requests + if (req.method === 'OPTIONS') { + return new NextResponse(null, { + status: 200, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }) + } + + // Get token from cookie + const token = req.cookies.get('auth-token')?.value + + if (!token) { + const response = NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + // Add CORS headers for error responses + response.headers.set('Access-Control-Allow-Origin', '*') + return response + } + + const payload = verifyToken(token) + if (!payload) { + const response = NextResponse.json({ error: 'Invalid token' }, { status: 401 }) + // Add CORS headers for error responses + response.headers.set('Access-Control-Allow-Origin', '*') + return response + } + + const result = await handler(req, payload.userId) + + // Add CORS headers to successful responses + result.headers.set('Access-Control-Allow-Origin', '*') + result.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') + result.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization') + + return result + } +} diff --git a/nextjs/src/shared/lib/prisma.ts b/nextjs/src/shared/lib/prisma.ts new file mode 100644 index 0000000..850ca56 --- /dev/null +++ b/nextjs/src/shared/lib/prisma.ts @@ -0,0 +1,18 @@ +import { PrismaClient } from '@prisma/client' + +const globalForPrisma = globalThis as unknown as { + prisma: PrismaClient | undefined +} + +export const prisma = + globalForPrisma.prisma ?? + new PrismaClient({ + datasources: { + db: { + url: process.env.DATABASE_URL + "?connection_limit=5&pool_timeout=20" + } + }, + log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], + }) + +if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma diff --git a/nextjs/src/shared/lib/s3.ts b/nextjs/src/shared/lib/s3.ts new file mode 100644 index 0000000..70ae432 --- /dev/null +++ b/nextjs/src/shared/lib/s3.ts @@ -0,0 +1,168 @@ +import { DeleteObjectCommand, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import { S3_CONFIG, s3Client } from '@/shared/config/s3'; + +/** + * Upload a file to S3 (Server-side) + * @param file - The file to upload (File or Buffer) + * @param folder - The folder path (e.g., 'images', 'audio', 'documents') + * @returns The public URL and key of the uploaded file + */ +export async function uploadFileServer( + file: File | Buffer, + fileName: string, + fileType: string, + folder: 'images' | 'audio' | 'documents' = 'documents' +): Promise<{ url: string; path: string }> { + try { + const fileExt = fileName.split('.').pop(); + const uniqueFileName = `${Date.now()}-${Math.random().toString(36).substring(2)}.${fileExt}`; + const fileKey = `jotion-uploads/${folder}/${uniqueFileName}`; + + // Convert File to Buffer if needed + let fileBuffer: Buffer; + if (file instanceof File) { + const arrayBuffer = await file.arrayBuffer(); + fileBuffer = Buffer.from(arrayBuffer); + } else { + fileBuffer = file; + } + + // Upload directly to S3 + const command = new PutObjectCommand({ + Bucket: S3_CONFIG.BUCKET_NAME, + Key: fileKey, + Body: fileBuffer, + ContentType: fileType, + }); + + await s3Client.send(command); + + // Return the public URL + const publicUrl = `${S3_CONFIG.BUCKET_URL}/${fileKey}`; + + return { + url: publicUrl, + path: fileKey, + }; + } catch (error: any) { + console.error('S3 upload error:', error); + throw new Error(`Failed to upload file: ${error.message}`); + } +} + +/** + * Upload a file to S3 (Client-side using presigned URL) + * @param file - The file to upload + * @param folder - The folder path (e.g., 'images', 'audio', 'documents') + * @returns The public URL and key of the uploaded file + */ +export async function uploadFile( + file: File, + folder: 'images' | 'audio' | 'documents' = 'documents' +): Promise<{ url: string; path: string }> { + try { + const fileExt = file.name.split('.').pop(); + const fileName = `${Date.now()}-${Math.random().toString(36).substring(2)}.${fileExt}`; + const fileKey = `jotion-uploads/${folder}/${fileName}`; + + // Get upload URL + const uploadUrlResponse = await fetch('/api/upload-url', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + fileName, + fileType: file.type, + folder: `jotion-uploads/${folder}`, + }), + }); + + if (!uploadUrlResponse.ok) { + throw new Error('Failed to get upload URL'); + } + + const { uploadUrl, fileKey: returnedFileKey } = await uploadUrlResponse.json(); + const finalFileKey = returnedFileKey || fileKey; + + // Upload to S3 using presigned URL + const uploadResponse = await fetch(uploadUrl, { + method: 'PUT', + body: file, + headers: { + 'Content-Type': file.type, + }, + }); + + if (!uploadResponse.ok) { + throw new Error(`Failed to upload file: ${uploadResponse.statusText}`); + } + + // Return the public URL + const publicUrl = `${S3_CONFIG.BUCKET_URL}/${finalFileKey}`; + + return { + url: publicUrl, + path: finalFileKey, + }; + } catch (error: any) { + console.error('S3 upload error:', error); + throw new Error(`Failed to upload file: ${error.message}`); + } +} + +/** + * Delete a file from S3 + * @param path - The file key to delete + */ +export async function deleteFile(path: string): Promise { + try { + const command = new DeleteObjectCommand({ + Bucket: S3_CONFIG.BUCKET_NAME, + Key: path, + }); + + await s3Client.send(command); + } catch (error: any) { + console.error('S3 delete error:', error); + throw new Error(`Failed to delete file: ${error.message}`); + } +} + +/** + * Get public URL for a file in S3 + * @param path - The file key + * @returns The public URL + */ +export function getFileUrl(path: string): string { + return `${S3_CONFIG.BUCKET_URL}/${path}`; +} + +/** + * Get signed download URL for a file in S3 + * @param path - The file key + * @returns The signed download URL + */ +export async function getDownloadUrl(path: string): Promise { + try { + const response = await fetch('/api/download-url', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ fileKey: path }), + }); + + if (!response.ok) { + throw new Error('Failed to get download URL'); + } + + const { downloadUrl } = await response.json(); + return downloadUrl; + } catch (error: any) { + console.error('Failed to get download URL:', error); + throw new Error(`Failed to get download URL: ${error.message}`); + } +} + diff --git a/nextjs/src/shared/lib/supabase.ts b/nextjs/src/shared/lib/supabase.ts new file mode 100644 index 0000000..c6a18b7 --- /dev/null +++ b/nextjs/src/shared/lib/supabase.ts @@ -0,0 +1,94 @@ +import { createClient } from '@supabase/supabase-js'; + +// Support both server-side and client-side environment variables +const supabaseUrl = + process.env.NEXT_PUBLIC_SUPABASE_URL || + process.env.SUPABASE_URL || + ''; + +const supabaseAnonKey = + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || + process.env.SUPABASE_ANON_KEY || + ''; + +if (!supabaseUrl || !supabaseAnonKey) { + console.warn('Supabase credentials are not set. Please set SUPABASE_URL and SUPABASE_ANON_KEY (or NEXT_PUBLIC_* variants) in your environment variables.'); + console.warn('Current values:', { + supabaseUrl: supabaseUrl ? 'set' : 'not set', + supabaseAnonKey: supabaseAnonKey ? 'set' : 'not set', + }); +} + +export const supabase = createClient(supabaseUrl, supabaseAnonKey); + +// Server-side Supabase client with service role (bypasses RLS) +// NEVER expose this to the client! +const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY || ''; +export const supabaseAdmin = supabaseServiceRoleKey + ? createClient(supabaseUrl, supabaseServiceRoleKey, { + auth: { + autoRefreshToken: false, + persistSession: false + } + }) + : null; + +// Storage bucket name for file uploads +export const STORAGE_BUCKET = 'jotion-files'; + +/** + * Upload a file to Supabase Storage + * @param file - The file to upload + * @param folder - The folder path (e.g., 'images', 'audio', 'documents') + * @returns The public URL of the uploaded file + */ +export async function uploadFile( + file: File, + folder: 'images' | 'audio' | 'documents' = 'documents' +): Promise<{ url: string; path: string }> { + const fileExt = file.name.split('.').pop(); + const fileName = `${Date.now()}-${Math.random().toString(36).substring(2)}.${fileExt}`; + const filePath = `${folder}/${fileName}`; + + // Use admin client to bypass RLS, fallback to regular client + const client = supabaseAdmin || supabase; + + const { data, error } = await client.storage + .from(STORAGE_BUCKET) + .upload(filePath, file, { + cacheControl: '3600', + upsert: false, + }); + + if (error) { + console.error('Supabase upload error:', error); + throw new Error(`Failed to upload file: ${error.message}`); + } + + const { data: { publicUrl } } = client.storage + .from(STORAGE_BUCKET) + .getPublicUrl(filePath); + + return { + url: publicUrl, + path: filePath, + }; +} + +/** + * Delete a file from Supabase Storage + * @param path - The file path to delete + */ +export async function deleteFile(path: string): Promise { + // Use admin client to bypass RLS, fallback to regular client + const client = supabaseAdmin || supabase; + + const { error } = await client.storage + .from(STORAGE_BUCKET) + .remove([path]); + + if (error) { + throw new Error(`Failed to delete file: ${error.message}`); + } +} + diff --git a/nextjs/src/shared/lib/utils.ts b/nextjs/src/shared/lib/utils.ts new file mode 100644 index 0000000..d084cca --- /dev/null +++ b/nextjs/src/shared/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/nextjs/src/shared/types/bookmark.ts b/nextjs/src/shared/types/bookmark.ts new file mode 100644 index 0000000..de9805b --- /dev/null +++ b/nextjs/src/shared/types/bookmark.ts @@ -0,0 +1,27 @@ +// Bookmark related types +export interface BookmarkAttributes { + url: string; + title?: string; + description?: string; + image?: string; + domain?: string; + author?: string; + loading?: boolean; + error?: string; +} + +export interface BookmarkMetadata { + url: string; + title?: string; + description?: string; + image?: string; + domain?: string; + author?: string; +} + +export interface BookmarkModalProps { + isOpen: boolean; + onClose: () => void; + onAddBookmark: (metadata: BookmarkMetadata) => void; +} + diff --git a/nextjs/src/shared/types/document.ts b/nextjs/src/shared/types/document.ts new file mode 100644 index 0000000..119757c --- /dev/null +++ b/nextjs/src/shared/types/document.ts @@ -0,0 +1,53 @@ +// Document related types +export type DocumentType = 'PAGE' | 'CODE_FILE'; + +export interface Document { + id: string; + title: string; + content?: any; // JSON content for rich text + icon?: string; + cover?: string; + isPublished: boolean; + isArchived: boolean; + createdAt: Date | string; + updatedAt: Date | string; + type: DocumentType; + filePath?: string; + fileContent?: string; + language?: string; + fileSize?: number; + parentId?: string; + folderId?: string; + userId: string; +} + +export interface DocumentWithRelations extends Document { + parent?: Document; + children?: Document[]; + folder?: { + id: string; + name: string; + icon?: string; + }; + user?: any; + _count?: { + documents: number; + }; +} + +export interface DocumentListItem { + id: string; + title: string; + icon?: string; + updatedAt: string; +} + +export interface HeadingItem { + id: string; + text: string; + level: number; + element?: HTMLElement; + children?: HeadingItem[]; + isExpanded?: boolean; +} + diff --git a/nextjs/src/shared/types/editor.ts b/nextjs/src/shared/types/editor.ts new file mode 100644 index 0000000..29c8b13 --- /dev/null +++ b/nextjs/src/shared/types/editor.ts @@ -0,0 +1,42 @@ +// Editor related types +import type { DocumentListItem } from './document'; + +export interface RichTextEditorProps { + content?: any; + onChange?: (content: any) => void; + placeholder?: string; + editable?: boolean; + readOnly?: boolean; + availableDocuments?: DocumentListItem[]; +} + +export interface DocumentSidebarProps { + content: any; + title: string; + lastSaved?: Date; + wordCount: number; + documentId?: string; + published?: boolean; + onShare?: () => void; + onUnshare?: () => void; + onCreateTemplate?: (templateData: any) => void | Promise; + onApplyTemplate?: (template: any) => void; +} + +export interface NotionBlockSelectorProps { + isOpen: boolean; + onClose: () => void; + onSelect: (blockType: string) => void; + position: { x: number; y: number }; +} + +export interface NotionFloatingToolbarProps { + isVisible: boolean; + position: { x: number; y: number }; + onFormat: (format: string) => void; + onClose: () => void; + activeFormats: string[]; + onSetTextColor?: (color: string) => void; + onSetFontFamily?: (fontFamily: string) => void; +} + diff --git a/nextjs/src/shared/types/folder.ts b/nextjs/src/shared/types/folder.ts new file mode 100644 index 0000000..c68f037 --- /dev/null +++ b/nextjs/src/shared/types/folder.ts @@ -0,0 +1,27 @@ +// Folder related types +export interface Folder { + id: string; + name: string; + icon?: string; + color?: string; + isArchived: boolean; + createdAt: Date | string; + updatedAt: Date | string; + parentId?: string; + userId: string; +} + +export interface FolderWithRelations extends Folder { + documents: Array<{ + id: string; + title: string; + icon?: string; + updatedAt: string; + }>; + children: FolderWithRelations[]; + _count: { + documents: number; + children: number; + }; +} + diff --git a/nextjs/src/shared/types/index.ts b/nextjs/src/shared/types/index.ts new file mode 100644 index 0000000..6c6f899 --- /dev/null +++ b/nextjs/src/shared/types/index.ts @@ -0,0 +1,7 @@ +// Export all types +export * from './document' +export * from './folder' +export * from './user' +export * from './template' +export * from './bookmark' +export * from './editor' \ No newline at end of file diff --git a/nextjs/src/shared/types/template.ts b/nextjs/src/shared/types/template.ts new file mode 100644 index 0000000..b523f59 --- /dev/null +++ b/nextjs/src/shared/types/template.ts @@ -0,0 +1,32 @@ +// Template related types +export interface Template { + id: string; + name: string; + description: string; + icon: React.ReactElement | string; + content: any; +} + +export interface DatabaseTemplate { + id: string; + name: string; + description: string; + category: string; + title: string; + content: any; + isPublic: boolean; + createdAt: string; + updatedAt: string; + userId: string; +} + +export interface TemplateBrowserProps { + isOpen: boolean; + onClose: () => void; + onSelectTemplate: (template: DatabaseTemplate) => void; +} + +export interface TemplateSelectorProps { + onSelectTemplate: (template: Template | DatabaseTemplate) => void; +} + diff --git a/nextjs/src/shared/types/user.ts b/nextjs/src/shared/types/user.ts new file mode 100644 index 0000000..e2c9a4c --- /dev/null +++ b/nextjs/src/shared/types/user.ts @@ -0,0 +1,15 @@ +// User related types +export interface User { + id: string; + email: string; + name?: string; + image?: string; +} + +export interface AuthContextType { + user: User | null; + isLoading: boolean; + login: (token: string, user: User) => void; + logout: () => void; +} + diff --git a/nextjs/src/shared/ui/SignedImage.tsx b/nextjs/src/shared/ui/SignedImage.tsx new file mode 100644 index 0000000..0451745 --- /dev/null +++ b/nextjs/src/shared/ui/SignedImage.tsx @@ -0,0 +1,86 @@ +"use client"; + +import React, { useState, useEffect } from 'react'; + +interface SignedImageProps { + fileKey: string; + alt: string; + className?: string; + style?: React.CSSProperties; + onError?: () => void; +} + +const SignedImage: React.FC = ({ + fileKey, + alt, + className = "", + style, + onError +}) => { + const [signedUrl, setSignedUrl] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const getSignedUrl = async () => { + try { + const response = await fetch('/api/download-url', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + fileKey: fileKey, + }), + }); + + if (!response.ok) { + throw new Error('서명된 URL 생성에 실패했습니다.'); + } + + const { downloadUrl } = await response.json(); + setSignedUrl(downloadUrl); + } catch (error) { + console.error('서명된 URL 생성 실패:', error); + setError('이미지를 불러올 수 없습니다.'); + onError?.(); + } finally { + setIsLoading(false); + } + }; + + getSignedUrl(); + }, [fileKey, onError]); + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (error || !signedUrl) { + return ( +
+ {error || '이미지 없음'} +
+ ); + } + + return ( + {alt} { + setError('이미지 로드 실패'); + onError?.(); + }} + /> + ); +}; + +export default SignedImage; + diff --git a/nextjs/src/shared/ui/button.tsx b/nextjs/src/shared/ui/button.tsx new file mode 100644 index 0000000..af239f1 --- /dev/null +++ b/nextjs/src/shared/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "../lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/nextjs/src/shared/ui/checkbox.tsx b/nextjs/src/shared/ui/checkbox.tsx new file mode 100644 index 0000000..a2d8e46 --- /dev/null +++ b/nextjs/src/shared/ui/checkbox.tsx @@ -0,0 +1,51 @@ +import * as React from "react" +import { cn } from "../lib/utils" +import { Check } from "lucide-react" + +export interface CheckboxProps + extends Omit, 'type'> { + onCheckedChange?: (checked: boolean) => void +} + +const Checkbox = React.forwardRef( + ({ className, onCheckedChange, checked, ...props }, ref) => { + const handleChange = (e: React.ChangeEvent) => { + onCheckedChange?.(e.target.checked) + props.onChange?.(e) + } + + return ( +
+ +
+ +
+
+ ) + } +) +Checkbox.displayName = "Checkbox" + +export { Checkbox } + diff --git a/nextjs/src/shared/ui/dialog.tsx b/nextjs/src/shared/ui/dialog.tsx new file mode 100644 index 0000000..4c6f3cb --- /dev/null +++ b/nextjs/src/shared/ui/dialog.tsx @@ -0,0 +1,119 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" +import { cn } from "../lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/nextjs/src/shared/ui/dropdown-menu.tsx b/nextjs/src/shared/ui/dropdown-menu.tsx new file mode 100644 index 0000000..2da0501 --- /dev/null +++ b/nextjs/src/shared/ui/dropdown-menu.tsx @@ -0,0 +1,200 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "../lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/nextjs/src/shared/ui/index.ts b/nextjs/src/shared/ui/index.ts new file mode 100644 index 0000000..2f4ab27 --- /dev/null +++ b/nextjs/src/shared/ui/index.ts @@ -0,0 +1,10 @@ +export * from './button' +export * from './checkbox' +export * from './dialog' +export * from './dropdown-menu' +export * from './input' +export * from './label' +export * from './textarea' +export * from './skeleton' +export * from './spinner' +export * from './mode-toggle' diff --git a/nextjs/src/shared/ui/input.tsx b/nextjs/src/shared/ui/input.tsx new file mode 100644 index 0000000..03f61d9 --- /dev/null +++ b/nextjs/src/shared/ui/input.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import { cn } from "../lib/utils" + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/nextjs/src/shared/ui/label.tsx b/nextjs/src/shared/ui/label.tsx new file mode 100644 index 0000000..395c742 --- /dev/null +++ b/nextjs/src/shared/ui/label.tsx @@ -0,0 +1,23 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "../lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/nextjs/src/shared/ui/mode-toggle.tsx b/nextjs/src/shared/ui/mode-toggle.tsx new file mode 100644 index 0000000..5636052 --- /dev/null +++ b/nextjs/src/shared/ui/mode-toggle.tsx @@ -0,0 +1,40 @@ +"use client" + +import * as React from "react" +import { Moon, Sun } from "lucide-react" +import { useTheme } from "next-themes" + +import { Button } from "./button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "./dropdown-menu" + +export function ModeToggle() { + const { setTheme } = useTheme() + + return ( + + + + + + setTheme("light")}> + Light + + setTheme("dark")}> + Dark + + setTheme("system")}> + System + + + + ) +} diff --git a/nextjs/src/shared/ui/skeleton.tsx b/nextjs/src/shared/ui/skeleton.tsx new file mode 100644 index 0000000..51eab5c --- /dev/null +++ b/nextjs/src/shared/ui/skeleton.tsx @@ -0,0 +1,218 @@ +import { cn } from "../lib/utils"; + +interface SkeletonProps { + className?: string; +} + +export const Skeleton = ({ className }: SkeletonProps) => { + return ( +
+ ); +}; + +// Document Card Skeleton +export const DocumentCardSkeleton = () => { + return ( +
+ + + +
+ ); +}; + +// Folder Card Skeleton +export const FolderCardSkeleton = () => { + return ( +
+ + +
+ ); +}; + +// Folder View Skeleton +export const FolderViewSkeleton = () => { + return ( +
+ {/* Folder header skeleton */} +
+ + +
+ + +
+
+ + {/* Subfolders section */} +
+ +
+ {Array.from({ length: 7 }).map((_, i) => ( + + ))} +
+
+ + {/* Documents section */} +
+ +
+ {Array.from({ length: 10 }).map((_, i) => ( + + ))} +
+
+
+ ); +}; + +// Documents Page Skeleton +export const DocumentsPageSkeleton = () => { + return ( +
+ {/* Folders section */} +
+
+ + +
+
+ {Array.from({ length: 7 }).map((_, i) => ( + + ))} +
+
+ + {/* Recent pages section */} +
+
+ + +
+
+ {Array.from({ length: 10 }).map((_, i) => ( + + ))} +
+
+
+ ); +}; + +// Document Detail Page Skeleton +export const DocumentDetailSkeleton = () => { + return ( +
+ {/* Header skeleton */} +
+
+
+
+ + +
+
+ + +
+
+
+
+ + {/* Content skeleton */} +
+
+ {/* Document header */} +
+
+ +
+ +
+ + +
+
+
+
+ + {/* Editor skeleton */} +
+
+ + + + + +
+ + +
+
+
+
+
+
+ ); +}; + +// Shared Document Page Skeleton +export const SharedDocumentSkeleton = () => { + return ( +
+ {/* Header skeleton */} +
+
+
+
+ +
+
+ +
+
+
+
+ + {/* Content skeleton */} +
+ {/* Document header */} +
+
+ +
+ +
+ + +
+
+
+
+ + {/* Content skeleton */} +
+
+ + + + +
+ + + +
+
+
+
+
+ ); +}; + diff --git a/nextjs/src/shared/ui/spinner.tsx b/nextjs/src/shared/ui/spinner.tsx new file mode 100644 index 0000000..92db7c4 --- /dev/null +++ b/nextjs/src/shared/ui/spinner.tsx @@ -0,0 +1,30 @@ +import { Loader } from "lucide-react"; +import { VariantProps,cva } from "class-variance-authority"; +import { cn } from "../lib/utils"; + +const spinnerVariants = cva( + "text-muted-foreground animate-spin", + { + variants: { + size: { + default: "h-4 w-4", + sm: "h-2 w-2", + lg: "h-6 w-6", + icon: "h-10 w-10" + } + }, + defaultVariants :{ + size: "default", + }, + }, +); + +interface SpinnerProps extends VariantProps {} + +export const Spinner = ({ + size, +}: SpinnerProps) => { + return ( + + ); +}; \ No newline at end of file diff --git a/nextjs/src/shared/ui/textarea.tsx b/nextjs/src/shared/ui/textarea.tsx new file mode 100644 index 0000000..850a3b7 --- /dev/null +++ b/nextjs/src/shared/ui/textarea.tsx @@ -0,0 +1,23 @@ +import * as React from "react" +import { cn } from "../lib/utils" + +export interface TextareaProps + extends React.TextareaHTMLAttributes {} + +const Textarea = React.forwardRef( + ({ className, ...props }, ref) => { + return ( +