From 8c13f6ad7118e4a6edc53bdefea95b64aaf6bd72 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Fri, 13 Feb 2026 12:10:22 -0700 Subject: [PATCH] Initial rtrips-online scaffold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Next.js 14 app with Prisma (PostgreSQL), Gemini NL parser, Tailwind dark theme, Docker + Traefik deployment config. Features: - Landing page with teal/cyan branding - NL input → Gemini 2.0 Flash parsing → structured trip data - Full Prisma schema (trips, destinations, itinerary, bookings, expenses, packing items, collaborators) - Trip CRUD API routes with all sub-entities - Trip dashboard with tabbed views (overview, itinerary, destinations, bookings, budget, packing) - Embedded rSpace canvas with full-screen mode - Docker multi-stage build with security hardening - Health check endpoint Co-Authored-By: Claude Opus 4.6 --- .env.example | 12 + .gitignore | 32 + Dockerfile | 38 + docker-compose.yml | 64 + next.config.mjs | 6 + package-lock.json | 1970 ++++++++++++++++++ package.json | 32 + postcss.config.mjs | 8 + prisma/schema.prisma | 225 ++ src/app/api/health/route.ts | 5 + src/app/api/trips/[id]/bookings/route.ts | 30 + src/app/api/trips/[id]/destinations/route.ts | 28 + src/app/api/trips/[id]/expenses/route.ts | 27 + src/app/api/trips/[id]/itinerary/route.ts | 28 + src/app/api/trips/[id]/packing/route.ts | 25 + src/app/api/trips/[id]/route.ts | 78 + src/app/api/trips/parse/route.ts | 24 + src/app/api/trips/route.ts | 132 ++ src/app/globals.css | 13 + src/app/layout.tsx | 33 + src/app/page.tsx | 138 ++ src/app/trips/[id]/canvas/page.tsx | 66 + src/app/trips/[id]/page.tsx | 462 ++++ src/app/trips/new/page.tsx | 90 + src/app/trips/page.tsx | 126 ++ src/components/CanvasEmbed.tsx | 35 + src/components/NLInput.tsx | 71 + src/components/ParsedTripPreview.tsx | 152 ++ src/lib/gemini.ts | 106 + src/lib/prisma.ts | 13 + src/lib/slug.ts | 9 + src/lib/types.ts | 32 + tailwind.config.ts | 17 + tsconfig.json | 26 + 34 files changed, 4153 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 next.config.mjs create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 prisma/schema.prisma create mode 100644 src/app/api/health/route.ts create mode 100644 src/app/api/trips/[id]/bookings/route.ts create mode 100644 src/app/api/trips/[id]/destinations/route.ts create mode 100644 src/app/api/trips/[id]/expenses/route.ts create mode 100644 src/app/api/trips/[id]/itinerary/route.ts create mode 100644 src/app/api/trips/[id]/packing/route.ts create mode 100644 src/app/api/trips/[id]/route.ts create mode 100644 src/app/api/trips/parse/route.ts create mode 100644 src/app/api/trips/route.ts create mode 100644 src/app/globals.css create mode 100644 src/app/layout.tsx create mode 100644 src/app/page.tsx create mode 100644 src/app/trips/[id]/canvas/page.tsx create mode 100644 src/app/trips/[id]/page.tsx create mode 100644 src/app/trips/new/page.tsx create mode 100644 src/app/trips/page.tsx create mode 100644 src/components/CanvasEmbed.tsx create mode 100644 src/components/NLInput.tsx create mode 100644 src/components/ParsedTripPreview.tsx create mode 100644 src/lib/gemini.ts create mode 100644 src/lib/prisma.ts create mode 100644 src/lib/slug.ts create mode 100644 src/lib/types.ts create mode 100644 tailwind.config.ts create mode 100644 tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8b52e53 --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +# Database +DATABASE_URL="postgresql://rtrips:changeme@localhost:5432/rtrips" + +# AI - Gemini 2.0 Flash for NL parsing +GEMINI_API_KEY="your-gemini-api-key" + +# rSpace integration +NEXT_PUBLIC_RSPACE_URL="https://rspace.online" +RSPACE_INTERNAL_URL="http://rspace-online:3000" + +# EncryptID +NEXT_PUBLIC_ENCRYPTID_SERVER_URL="https://encryptid.jeffemmett.com" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d8762a0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env +.env*.local + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1a51686 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +FROM node:20-alpine AS base + +# Dependencies stage +FROM base AS deps +WORKDIR /app +COPY package.json package-lock.json* ./ +COPY prisma ./prisma/ +RUN npm ci || npm install + +# Build stage +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN npx prisma generate +RUN npm run build + +# Production stage +FROM base AS runner +WORKDIR /app +ENV NODE_ENV=production + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +COPY --from=builder /app/prisma ./prisma +COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma +COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma + +USER nextjs +EXPOSE 3000 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +CMD ["node", "server.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8e5a167 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,64 @@ +services: + rtrips: + build: . + container_name: rtrips-online + restart: unless-stopped + environment: + - DATABASE_URL=postgresql://rtrips:${DB_PASSWORD}@rtrips-postgres:5432/rtrips + - GEMINI_API_KEY=${GEMINI_API_KEY} + - NEXT_PUBLIC_RSPACE_URL=${NEXT_PUBLIC_RSPACE_URL:-https://rspace.online} + - RSPACE_INTERNAL_URL=${RSPACE_INTERNAL_URL:-http://rspace-online:3000} + - NEXT_PUBLIC_ENCRYPTID_SERVER_URL=${NEXT_PUBLIC_ENCRYPTID_SERVER_URL:-https://encryptid.jeffemmett.com} + labels: + - "traefik.enable=true" + - "traefik.http.routers.rtrips.rule=Host(`rtrips.online`) || Host(`www.rtrips.online`)" + - "traefik.http.services.rtrips.loadbalancer.server.port=3000" + networks: + - traefik-public + - rtrips-internal + depends_on: + rtrips-postgres: + condition: service_healthy + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + read_only: true + tmpfs: + - /tmp + + rtrips-postgres: + image: postgres:16-alpine + container_name: rtrips-postgres + restart: unless-stopped + environment: + - POSTGRES_USER=rtrips + - POSTGRES_PASSWORD=${DB_PASSWORD} + - POSTGRES_DB=rtrips + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - rtrips-internal + healthcheck: + test: ["CMD-SHELL", "pg_isready -U rtrips -d rtrips"] + interval: 5s + timeout: 5s + retries: 5 + cap_drop: + - ALL + cap_add: + - DAC_OVERRIDE + - FOWNER + - SETGID + - SETUID + security_opt: + - no-new-privileges:true + +networks: + traefik-public: + external: true + rtrips-internal: + internal: true + +volumes: + postgres_data: diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 0000000..e25a6a2 --- /dev/null +++ b/next.config.mjs @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'standalone', +}; + +export default nextConfig; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..35606ff --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1970 @@ +{ + "name": "rtrips-online", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rtrips-online", + "version": "0.1.0", + "dependencies": { + "@prisma/client": "^6.19.2", + "date-fns": "^4.1.0", + "nanoid": "^5.0.9", + "next": "14.2.35", + "react": "^18", + "react-dom": "^18", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "postcss": "^8", + "prisma": "^6.19.2", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz", + "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz", + "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz", + "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz", + "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz", + "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz", + "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz", + "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz", + "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz", + "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz", + "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@prisma/client": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.2.tgz", + "integrity": "sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.2.tgz", + "integrity": "sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.18.4", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.2.tgz", + "integrity": "sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.2.tgz", + "integrity": "sha512-TTkJ8r+uk/uqczX40wb+ODG0E0icVsMgwCTyTHXehaEfb0uo80M9g1aW1tEJrxmFHeOZFXdI2sTA1j1AgcHi4A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.2", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/fetch-engine": "6.19.2", + "@prisma/get-platform": "6.19.2" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", + "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.2.tgz", + "integrity": "sha512-h4Ff4Pho+SR1S8XerMCC12X//oY2bG3Iug/fUnudfcXEUnIeRiBdXHFdGlGOgQ3HqKgosTEhkZMvGM9tWtYC+Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.2", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/get-platform": "6.19.2" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.2.tgz", + "integrity": "sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.2" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", + "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.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==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "license": "MIT", + "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/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/effect": { + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", + "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "license": "MIT", + "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-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/next": { + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz", + "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==", + "license": "MIT", + "dependencies": { + "@next/env": "14.2.35", + "@swc/helpers": "0.5.5", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.2.33", + "@next/swc-darwin-x64": "14.2.33", + "@next/swc-linux-arm64-gnu": "14.2.33", + "@next/swc-linux-arm64-musl": "14.2.33", + "@next/swc-linux-x64-gnu": "14.2.33", + "@next/swc-linux-x64-musl": "14.2.33", + "@next/swc-win32-arm64-msvc": "14.2.33", + "@next/swc-win32-ia32-msvc": "14.2.33", + "@next/swc-win32-x64-msvc": "14.2.33" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nypm": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", + "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.2.0", + "pathe": "^2.0.3", + "tinyexec": "^1.0.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", + "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", + "devOptional": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "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==", + "devOptional": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "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==", + "devOptional": true, + "license": "MIT", + "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==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "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==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "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==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/prisma": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.2.tgz", + "integrity": "sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.19.2", + "@prisma/engines": "6.19.2" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "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/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tailwindcss/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==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tailwindcss/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==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/tailwindcss/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/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==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/zustand": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", + "license": "MIT", + "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/package.json b/package.json new file mode 100644 index 0000000..b706046 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "rtrips-online", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "db:push": "npx prisma db push", + "db:migrate": "npx prisma migrate dev", + "db:studio": "npx prisma studio" + }, + "dependencies": { + "@prisma/client": "^6.19.2", + "date-fns": "^4.1.0", + "nanoid": "^5.0.9", + "next": "14.2.35", + "react": "^18", + "react-dom": "^18", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "postcss": "^8", + "prisma": "^6.19.2", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } +} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..1a69fd2 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..25c139c --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,225 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// ─── Users ────────────────────────────────────────────────────────── + +model User { + id String @id @default(cuid()) + did String @unique // EncryptID DID + username String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + trips TripCollaborator[] + expenses Expense[] + packingItems PackingItem[] +} + +// ─── Trips ────────────────────────────────────────────────────────── + +model Trip { + id String @id @default(cuid()) + title String + slug String @unique + description String? @db.Text + rawInput String? @db.Text // Original NL input preserved + startDate DateTime? + endDate DateTime? + budgetTotal Float? + budgetCurrency String @default("USD") + status TripStatus @default(PLANNING) + canvasSlug String? // rspace community slug + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + collaborators TripCollaborator[] + destinations Destination[] + itineraryItems ItineraryItem[] + bookings Booking[] + expenses Expense[] + packingItems PackingItem[] +} + +enum TripStatus { + PLANNING + BOOKED + IN_PROGRESS + COMPLETED + CANCELLED +} + +model TripCollaborator { + id String @id @default(cuid()) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + tripId String + trip Trip @relation(fields: [tripId], references: [id], onDelete: Cascade) + role CollaboratorRole @default(MEMBER) + joinedAt DateTime @default(now()) + + @@unique([userId, tripId]) + @@index([tripId]) +} + +enum CollaboratorRole { + OWNER + EDITOR + VIEWER + MEMBER +} + +// ─── Destinations ─────────────────────────────────────────────────── + +model Destination { + id String @id @default(cuid()) + tripId String + trip Trip @relation(fields: [tripId], references: [id], onDelete: Cascade) + name String + country String? + lat Float? + lng Float? + arrivalDate DateTime? + departureDate DateTime? + notes String? @db.Text + sortOrder Int @default(0) + canvasShapeId String? // ID of folk-destination on canvas + createdAt DateTime @default(now()) + + itineraryItems ItineraryItem[] + bookings Booking[] + + @@index([tripId]) +} + +// ─── Itinerary ────────────────────────────────────────────────────── + +model ItineraryItem { + id String @id @default(cuid()) + tripId String + trip Trip @relation(fields: [tripId], references: [id], onDelete: Cascade) + destinationId String? + destination Destination? @relation(fields: [destinationId], references: [id], onDelete: SetNull) + title String + description String? @db.Text + date DateTime? + startTime String? // "09:00" format + endTime String? // "17:00" format + category ItineraryCategory @default(ACTIVITY) + sortOrder Int @default(0) + canvasShapeId String? + createdAt DateTime @default(now()) + + @@index([tripId]) + @@index([destinationId]) +} + +enum ItineraryCategory { + FLIGHT + TRANSPORT + ACCOMMODATION + ACTIVITY + MEAL + FREE_TIME + OTHER +} + +// ─── Bookings ─────────────────────────────────────────────────────── + +model Booking { + id String @id @default(cuid()) + tripId String + trip Trip @relation(fields: [tripId], references: [id], onDelete: Cascade) + destinationId String? + destination Destination? @relation(fields: [destinationId], references: [id], onDelete: SetNull) + type BookingType + provider String? // "Air Canada", "Booking.com" + confirmationNumber String? + details String? @db.Text // JSON for flexible data + cost Float? + currency String @default("USD") + startDate DateTime? + endDate DateTime? + status BookingStatus @default(PLANNED) + canvasShapeId String? + createdAt DateTime @default(now()) + + @@index([tripId]) +} + +enum BookingType { + FLIGHT + HOTEL + CAR_RENTAL + TRAIN + BUS + FERRY + ACTIVITY + RESTAURANT + OTHER +} + +enum BookingStatus { + PLANNED + BOOKED + CONFIRMED + CANCELLED +} + +// ─── Expenses ─────────────────────────────────────────────────────── + +model Expense { + id String @id @default(cuid()) + tripId String + trip Trip @relation(fields: [tripId], references: [id], onDelete: Cascade) + paidById String? + paidBy User? @relation(fields: [paidById], references: [id], onDelete: SetNull) + description String + amount Float + currency String @default("USD") + category ExpenseCategory @default(OTHER) + date DateTime? + splitType SplitType @default(EQUAL) + createdAt DateTime @default(now()) + + @@index([tripId]) +} + +enum ExpenseCategory { + FLIGHT + ACCOMMODATION + FOOD + TRANSPORT + ACTIVITY + SHOPPING + OTHER +} + +enum SplitType { + EQUAL + CUSTOM + INDIVIDUAL +} + +// ─── Packing ──────────────────────────────────────────────────────── + +model PackingItem { + id String @id @default(cuid()) + tripId String + trip Trip @relation(fields: [tripId], references: [id], onDelete: Cascade) + addedById String? + addedBy User? @relation(fields: [addedById], references: [id], onDelete: SetNull) + name String + category String? // "Clothing", "Electronics", "Documents" + packed Boolean @default(false) + quantity Int @default(1) + sortOrder Int @default(0) + createdAt DateTime @default(now()) + + @@index([tripId]) +} diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts new file mode 100644 index 0000000..7ed3efb --- /dev/null +++ b/src/app/api/health/route.ts @@ -0,0 +1,5 @@ +import { NextResponse } from 'next/server'; + +export async function GET() { + return NextResponse.json({ status: 'ok', service: 'rtrips-online' }); +} diff --git a/src/app/api/trips/[id]/bookings/route.ts b/src/app/api/trips/[id]/bookings/route.ts new file mode 100644 index 0000000..ab39486 --- /dev/null +++ b/src/app/api/trips/[id]/bookings/route.ts @@ -0,0 +1,30 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +export async function POST( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const body = await request.json(); + const booking = await prisma.booking.create({ + data: { + tripId: params.id, + destinationId: body.destinationId, + type: body.type, + provider: body.provider, + confirmationNumber: body.confirmationNumber, + details: body.details, + cost: body.cost, + currency: body.currency || 'USD', + startDate: body.startDate ? new Date(body.startDate) : null, + endDate: body.endDate ? new Date(body.endDate) : null, + status: body.status || 'PLANNED', + }, + }); + return NextResponse.json(booking, { status: 201 }); + } catch (error) { + console.error('Create booking error:', error); + return NextResponse.json({ error: 'Failed to create booking' }, { status: 500 }); + } +} diff --git a/src/app/api/trips/[id]/destinations/route.ts b/src/app/api/trips/[id]/destinations/route.ts new file mode 100644 index 0000000..15d86df --- /dev/null +++ b/src/app/api/trips/[id]/destinations/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +export async function POST( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const body = await request.json(); + const destination = await prisma.destination.create({ + data: { + tripId: params.id, + name: body.name, + country: body.country, + lat: body.lat, + lng: body.lng, + arrivalDate: body.arrivalDate ? new Date(body.arrivalDate) : null, + departureDate: body.departureDate ? new Date(body.departureDate) : null, + notes: body.notes, + sortOrder: body.sortOrder ?? 0, + }, + }); + return NextResponse.json(destination, { status: 201 }); + } catch (error) { + console.error('Create destination error:', error); + return NextResponse.json({ error: 'Failed to create destination' }, { status: 500 }); + } +} diff --git a/src/app/api/trips/[id]/expenses/route.ts b/src/app/api/trips/[id]/expenses/route.ts new file mode 100644 index 0000000..6792b53 --- /dev/null +++ b/src/app/api/trips/[id]/expenses/route.ts @@ -0,0 +1,27 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +export async function POST( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const body = await request.json(); + const expense = await prisma.expense.create({ + data: { + tripId: params.id, + paidById: body.paidById, + description: body.description, + amount: body.amount, + currency: body.currency || 'USD', + category: body.category || 'OTHER', + date: body.date ? new Date(body.date) : null, + splitType: body.splitType || 'EQUAL', + }, + }); + return NextResponse.json(expense, { status: 201 }); + } catch (error) { + console.error('Create expense error:', error); + return NextResponse.json({ error: 'Failed to create expense' }, { status: 500 }); + } +} diff --git a/src/app/api/trips/[id]/itinerary/route.ts b/src/app/api/trips/[id]/itinerary/route.ts new file mode 100644 index 0000000..47559a9 --- /dev/null +++ b/src/app/api/trips/[id]/itinerary/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +export async function POST( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const body = await request.json(); + const item = await prisma.itineraryItem.create({ + data: { + tripId: params.id, + destinationId: body.destinationId, + title: body.title, + description: body.description, + date: body.date ? new Date(body.date) : null, + startTime: body.startTime, + endTime: body.endTime, + category: body.category || 'ACTIVITY', + sortOrder: body.sortOrder ?? 0, + }, + }); + return NextResponse.json(item, { status: 201 }); + } catch (error) { + console.error('Create itinerary item error:', error); + return NextResponse.json({ error: 'Failed to create itinerary item' }, { status: 500 }); + } +} diff --git a/src/app/api/trips/[id]/packing/route.ts b/src/app/api/trips/[id]/packing/route.ts new file mode 100644 index 0000000..3a04cf5 --- /dev/null +++ b/src/app/api/trips/[id]/packing/route.ts @@ -0,0 +1,25 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +export async function POST( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const body = await request.json(); + const item = await prisma.packingItem.create({ + data: { + tripId: params.id, + addedById: body.addedById, + name: body.name, + category: body.category, + quantity: body.quantity ?? 1, + sortOrder: body.sortOrder ?? 0, + }, + }); + return NextResponse.json(item, { status: 201 }); + } catch (error) { + console.error('Create packing item error:', error); + return NextResponse.json({ error: 'Failed to create packing item' }, { status: 500 }); + } +} diff --git a/src/app/api/trips/[id]/route.ts b/src/app/api/trips/[id]/route.ts new file mode 100644 index 0000000..b546204 --- /dev/null +++ b/src/app/api/trips/[id]/route.ts @@ -0,0 +1,78 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +export async function GET( + _request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const trip = await prisma.trip.findUnique({ + where: { id: params.id }, + include: { + destinations: { orderBy: { sortOrder: 'asc' } }, + itineraryItems: { orderBy: [{ date: 'asc' }, { sortOrder: 'asc' }] }, + bookings: { orderBy: { startDate: 'asc' } }, + expenses: { orderBy: { date: 'desc' } }, + packingItems: { orderBy: [{ category: 'asc' }, { sortOrder: 'asc' }] }, + collaborators: { include: { user: true } }, + }, + }); + + if (!trip) { + return NextResponse.json({ error: 'Trip not found' }, { status: 404 }); + } + + return NextResponse.json(trip); + } catch (error) { + console.error('Get trip error:', error); + return NextResponse.json( + { error: 'Failed to get trip' }, + { status: 500 } + ); + } +} + +export async function PUT( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const body = await request.json(); + const trip = await prisma.trip.update({ + where: { id: params.id }, + data: { + title: body.title, + description: body.description, + startDate: body.startDate ? new Date(body.startDate) : undefined, + endDate: body.endDate ? new Date(body.endDate) : undefined, + budgetTotal: body.budgetTotal, + budgetCurrency: body.budgetCurrency, + status: body.status, + }, + }); + + return NextResponse.json(trip); + } catch (error) { + console.error('Update trip error:', error); + return NextResponse.json( + { error: 'Failed to update trip' }, + { status: 500 } + ); + } +} + +export async function DELETE( + _request: NextRequest, + { params }: { params: { id: string } } +) { + try { + await prisma.trip.delete({ where: { id: params.id } }); + return NextResponse.json({ ok: true }); + } catch (error) { + console.error('Delete trip error:', error); + return NextResponse.json( + { error: 'Failed to delete trip' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/trips/parse/route.ts b/src/app/api/trips/parse/route.ts new file mode 100644 index 0000000..2366528 --- /dev/null +++ b/src/app/api/trips/parse/route.ts @@ -0,0 +1,24 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { parseTrip } from '@/lib/gemini'; + +export async function POST(request: NextRequest) { + try { + const { text } = await request.json(); + + if (!text || typeof text !== 'string' || text.trim().length === 0) { + return NextResponse.json( + { error: 'Please provide a trip description' }, + { status: 400 } + ); + } + + const parsed = await parseTrip(text.trim()); + return NextResponse.json(parsed); + } catch (error) { + console.error('Parse error:', error); + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Failed to parse trip' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/trips/route.ts b/src/app/api/trips/route.ts new file mode 100644 index 0000000..7e2c7ae --- /dev/null +++ b/src/app/api/trips/route.ts @@ -0,0 +1,132 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { generateSlug } from '@/lib/slug'; +import { ParsedTrip } from '@/lib/types'; + +export async function GET() { + try { + const trips = await prisma.trip.findMany({ + include: { + destinations: { orderBy: { sortOrder: 'asc' } }, + collaborators: { include: { user: true } }, + _count: { + select: { + itineraryItems: true, + bookings: true, + expenses: true, + packingItems: true, + }, + }, + }, + orderBy: { updatedAt: 'desc' }, + }); + + return NextResponse.json(trips); + } catch (error) { + console.error('List trips error:', error); + return NextResponse.json( + { error: 'Failed to list trips' }, + { status: 500 } + ); + } +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { parsed, rawInput }: { parsed: ParsedTrip; rawInput: string } = body; + + if (!parsed?.title) { + return NextResponse.json( + { error: 'Trip title is required' }, + { status: 400 } + ); + } + + // Generate unique slug + let slug = generateSlug(parsed.title); + const existing = await prisma.trip.findUnique({ where: { slug } }); + if (existing) { + slug = `${slug}-${Date.now().toString(36)}`; + } + + // Create trip with all related entities in a transaction + const trip = await prisma.$transaction(async (tx) => { + const newTrip = await tx.trip.create({ + data: { + title: parsed.title, + slug, + description: parsed.destinations.map(d => d.name).join(' → '), + rawInput: rawInput || null, + startDate: parsed.startDate ? new Date(parsed.startDate) : null, + endDate: parsed.endDate ? new Date(parsed.endDate) : null, + budgetTotal: parsed.budgetTotal, + budgetCurrency: parsed.budgetCurrency || 'USD', + status: 'PLANNING', + }, + }); + + // Create destinations + if (parsed.destinations.length > 0) { + await tx.destination.createMany({ + data: parsed.destinations.map((d, i) => ({ + tripId: newTrip.id, + name: d.name, + country: d.country, + arrivalDate: d.arrivalDate ? new Date(d.arrivalDate) : null, + departureDate: d.departureDate ? new Date(d.departureDate) : null, + sortOrder: i, + })), + }); + } + + // Create itinerary items + if (parsed.itineraryItems.length > 0) { + await tx.itineraryItem.createMany({ + data: parsed.itineraryItems.map((item, i) => ({ + tripId: newTrip.id, + title: item.title, + category: item.category as never, + date: item.date ? new Date(item.date) : null, + description: item.description, + sortOrder: i, + })), + }); + } + + // Create bookings + if (parsed.bookings.length > 0) { + await tx.booking.createMany({ + data: parsed.bookings.map((b) => ({ + tripId: newTrip.id, + type: b.type as never, + provider: b.provider, + details: b.details, + cost: b.cost, + status: 'PLANNED', + })), + }); + } + + return newTrip; + }); + + // Re-fetch with relations + const fullTrip = await prisma.trip.findUnique({ + where: { id: trip.id }, + include: { + destinations: { orderBy: { sortOrder: 'asc' } }, + itineraryItems: { orderBy: { sortOrder: 'asc' } }, + bookings: true, + }, + }); + + return NextResponse.json(fullTrip, { status: 201 }); + } catch (error) { + console.error('Create trip error:', error); + return NextResponse.json( + { error: 'Failed to create trip' }, + { status: 500 } + ); + } +} diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..d477be5 --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --background: #0a0a0a; + --foreground: #ededed; +} + +body { + color: var(--foreground); + background: var(--background); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..1bc6a49 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,33 @@ +import type { Metadata } from 'next' +import { Inter } from 'next/font/google' +import './globals.css' + +const inter = Inter({ + subsets: ['latin'], + variable: '--font-inter', +}) + +export const metadata: Metadata = { + title: 'rTrips - Collaborative Trip Planning', + description: 'Plan trips together with natural language input, structured itineraries, and a real-time collaborative canvas. Describe your trip and let AI structure it for you.', + openGraph: { + title: 'rTrips - Collaborative Trip Planning', + description: 'Plan trips together with natural language input and real-time collaborative canvas.', + type: 'website', + url: 'https://rtrips.online', + }, +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + {children} + + + ) +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..4bb7f79 --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,138 @@ +import Link from 'next/link' + +export default function Home() { + return ( +
+ {/* Nav */} + + + {/* Hero */} +
+
+

+ Plan Your Trip, Naturally +

+

+ Describe your dream trip in plain language. We'll structure it into + itineraries, budgets, and bookings — then give you a collaborative + canvas to plan together in real-time. +

+
+ + Start Planning + + + My Trips + +
+
+
+ + {/* How it Works */} +
+

How It Works

+
+ {/* Describe */} +
+
+ + + +
+

Describe It

+

+ Tell us about your trip in natural language. “Fly from Toronto to Bali + for 2 weeks in March, budget $3000.” We parse it into structured data + you can refine. +

+
+ + {/* Structure */} +
+
+ + + +
+

We Structure It

+

+ AI extracts destinations, dates, budgets, and bookings into organized views. + Edit itineraries, track expenses, manage packing lists — all structured + and searchable. +

+
+ + {/* Collaborate */} +
+
+ + + +
+

Collaborate on Canvas

+

+ Open the collaborative canvas to plan visually with your travel partners. + Drag destinations, connect itineraries, and brainstorm together in real-time + or async. +

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

Ready to plan your next adventure?

+

+ Just describe where you want to go. We'll handle the rest. +

+ + Plan a Trip + +
+ + {/* Footer */} +
+
+ rTrips.online +
+ Plan a Trip + My Trips + rSpace +
+
+
+
+ ) +} diff --git a/src/app/trips/[id]/canvas/page.tsx b/src/app/trips/[id]/canvas/page.tsx new file mode 100644 index 0000000..e24aab4 --- /dev/null +++ b/src/app/trips/[id]/canvas/page.tsx @@ -0,0 +1,66 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import { CanvasEmbed } from '@/components/CanvasEmbed'; + +export default function FullScreenCanvas() { + const params = useParams(); + const router = useRouter(); + const [canvasSlug, setCanvasSlug] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetch(`/api/trips/${params.id}`) + .then((res) => res.json()) + .then((trip) => setCanvasSlug(trip.canvasSlug)) + .catch(console.error) + .finally(() => setLoading(false)); + }, [params.id]); + + if (loading) { + return ( +
+ + + + +
+ ); + } + + if (!canvasSlug) { + return ( +
+
+

No canvas linked to this trip yet.

+ +
+
+ ); + } + + return ( +
+ {/* Floating back button */} +
+ +
+ + +
+ ); +} diff --git a/src/app/trips/[id]/page.tsx b/src/app/trips/[id]/page.tsx new file mode 100644 index 0000000..7822c50 --- /dev/null +++ b/src/app/trips/[id]/page.tsx @@ -0,0 +1,462 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useParams } from 'next/navigation'; +import Link from 'next/link'; +import { format } from 'date-fns'; +import { CanvasEmbed } from '@/components/CanvasEmbed'; + +interface Trip { + id: string; + title: string; + slug: string; + description: string | null; + startDate: string | null; + endDate: string | null; + budgetTotal: number | null; + budgetCurrency: string; + status: string; + canvasSlug: string | null; + destinations: Destination[]; + itineraryItems: ItineraryItem[]; + bookings: Booking[]; + expenses: Expense[]; + packingItems: PackingItem[]; +} + +interface Destination { + id: string; + name: string; + country: string | null; + arrivalDate: string | null; + departureDate: string | null; + notes: string | null; +} + +interface ItineraryItem { + id: string; + title: string; + description: string | null; + date: string | null; + startTime: string | null; + endTime: string | null; + category: string; +} + +interface Booking { + id: string; + type: string; + provider: string | null; + confirmationNumber: string | null; + details: string | null; + cost: number | null; + currency: string; + startDate: string | null; + endDate: string | null; + status: string; +} + +interface Expense { + id: string; + description: string; + amount: number; + currency: string; + category: string; + date: string | null; +} + +interface PackingItem { + id: string; + name: string; + category: string | null; + packed: boolean; + quantity: number; +} + +type Tab = 'overview' | 'itinerary' | 'destinations' | 'bookings' | 'budget' | 'packing'; + +const TABS: { key: Tab; label: string }[] = [ + { key: 'overview', label: 'Overview' }, + { key: 'itinerary', label: 'Itinerary' }, + { key: 'destinations', label: 'Destinations' }, + { key: 'bookings', label: 'Bookings' }, + { key: 'budget', label: 'Budget' }, + { key: 'packing', label: 'Packing' }, +]; + +const CATEGORY_ICONS: Record = { + FLIGHT: '\u2708\uFE0F', + TRANSPORT: '\uD83D\uDE8C', + ACCOMMODATION: '\uD83C\uDFE8', + ACTIVITY: '\uD83C\uDFAF', + MEAL: '\uD83C\uDF7D\uFE0F', + FREE_TIME: '\u2600\uFE0F', + OTHER: '\uD83D\uDCCC', +}; + +const BOOKING_TYPE_COLORS: Record = { + FLIGHT: 'bg-blue-500/20 text-blue-300', + HOTEL: 'bg-purple-500/20 text-purple-300', + CAR_RENTAL: 'bg-amber-500/20 text-amber-300', + ACTIVITY: 'bg-teal-500/20 text-teal-300', + RESTAURANT: 'bg-orange-500/20 text-orange-300', + OTHER: 'bg-slate-500/20 text-slate-300', +}; + +const STATUS_BADGES: Record = { + PLANNED: 'bg-slate-500/20 text-slate-300', + BOOKED: 'bg-blue-500/20 text-blue-300', + CONFIRMED: 'bg-emerald-500/20 text-emerald-300', + CANCELLED: 'bg-red-500/20 text-red-300', +}; + +export default function TripDashboard() { + const params = useParams(); + const [trip, setTrip] = useState(null); + const [loading, setLoading] = useState(true); + const [activeTab, setActiveTab] = useState('overview'); + const [showCanvas, setShowCanvas] = useState(false); + + useEffect(() => { + fetch(`/api/trips/${params.id}`) + .then((res) => res.json()) + .then(setTrip) + .catch(console.error) + .finally(() => setLoading(false)); + }, [params.id]); + + if (loading) { + return ( +
+ + + + +
+ ); + } + + if (!trip) { + return ( +
+
+

Trip not found

+ Back to My Trips +
+
+ ); + } + + const totalExpenses = trip.expenses.reduce((sum, e) => sum + e.amount, 0); + const packedCount = trip.packingItems.filter((i) => i.packed).length; + + return ( +
+ {/* Nav */} + + +
+
+ {/* Main content */} +
+ {/* Header */} +
+
+
+

{trip.title}

+ {trip.description &&

{trip.description}

} +
+ + {trip.status.replace('_', ' ')} + +
+
+ {trip.startDate && ( + {format(new Date(trip.startDate), 'MMM d')} – {trip.endDate ? format(new Date(trip.endDate), 'MMM d, yyyy') : '...'} + )} + {trip.budgetTotal && ( + Budget: {trip.budgetCurrency} {trip.budgetTotal.toLocaleString()} + )} + {trip.destinations.length} destinations +
+
+ + {/* Tabs */} +
+ {TABS.map((tab) => ( + + ))} +
+ + {/* Tab Content */} +
+ {activeTab === 'overview' && ( +
+
+

Destinations

+ {trip.destinations.map((d) => ( +
+
+ {d.name} + {d.country && ({d.country})} +
+ ))} +
+
+

Budget

+ {trip.budgetTotal && ( + <> +
+ Spent + {trip.budgetCurrency} {totalExpenses.toLocaleString()} / {trip.budgetTotal.toLocaleString()} +
+
+
+
+ + )} +
+
+

Bookings

+

{trip.bookings.length}

+

{trip.bookings.filter(b => b.status === 'CONFIRMED').length} confirmed

+
+
+

Packing

+

{packedCount}/{trip.packingItems.length}

+

items packed

+
+
+ )} + + {activeTab === 'itinerary' && ( +
+ {trip.itineraryItems.length === 0 ? ( +

No itinerary items yet

+ ) : ( + trip.itineraryItems.map((item) => ( +
+ {CATEGORY_ICONS[item.category] || CATEGORY_ICONS.OTHER} +
+

{item.title}

+ {item.description &&

{item.description}

} +
+ {item.date && {format(new Date(item.date), 'MMM d, yyyy')}} + {item.startTime && {item.startTime}{item.endTime && ` - ${item.endTime}`}} +
+
+ {item.category} +
+ )) + )} +
+ )} + + {activeTab === 'destinations' && ( +
+ {trip.destinations.map((dest, i) => ( +
+
+
+ {i + 1} +
+
+

{dest.name}

+ {dest.country &&

{dest.country}

} + {(dest.arrivalDate || dest.departureDate) && ( +

+ {dest.arrivalDate && format(new Date(dest.arrivalDate), 'MMM d')} + {dest.arrivalDate && dest.departureDate && ' \u2192 '} + {dest.departureDate && format(new Date(dest.departureDate), 'MMM d, yyyy')} +

+ )} + {dest.notes &&

{dest.notes}

} +
+
+
+ ))} +
+ )} + + {activeTab === 'bookings' && ( +
+ {trip.bookings.length === 0 ? ( +

No bookings yet

+ ) : ( + trip.bookings.map((b) => ( +
+
+
+ + {b.type} + + {b.provider || b.details || 'TBD'} +
+
+ {b.cost && {b.currency} {b.cost.toLocaleString()}} + + {b.status} + +
+
+ {b.confirmationNumber && ( +

Confirmation: {b.confirmationNumber}

+ )} + {b.details && b.provider && ( +

{b.details}

+ )} +
+ )) + )} +
+ )} + + {activeTab === 'budget' && ( +
+ {trip.budgetTotal && ( +
+
+ Total Budget + {trip.budgetCurrency} {trip.budgetTotal.toLocaleString()} +
+
+ Spent + {trip.budgetCurrency} {totalExpenses.toLocaleString()} +
+
+
0.9 ? 'bg-red-500' : 'bg-teal-500' + }`} + style={{ width: `${Math.min((totalExpenses / trip.budgetTotal) * 100, 100)}%` }} + /> +
+

+ {trip.budgetCurrency} {(trip.budgetTotal - totalExpenses).toLocaleString()} remaining +

+
+ )} +
+ {trip.expenses.length === 0 ? ( +

No expenses tracked yet

+ ) : ( + trip.expenses.map((e) => ( +
+
+

{e.description}

+

{e.category} {e.date && `\u00B7 ${format(new Date(e.date), 'MMM d')}`}

+
+ {e.currency} {e.amount.toLocaleString()} +
+ )) + )} +
+
+ )} + + {activeTab === 'packing' && ( +
+ {trip.packingItems.length === 0 ? ( +

No packing items yet

+ ) : ( + <> +
+ {packedCount} of {trip.packingItems.length} packed +
+
0 ? (packedCount / trip.packingItems.length) * 100 : 0}%` }} + /> +
+
+ {trip.packingItems.map((item) => ( +
+ + {item.name} + {item.quantity > 1 && x{item.quantity}} + {item.category && {item.category}} +
+ ))} + + )} +
+ )} +
+
+ + {/* Canvas sidebar */} + {showCanvas && trip.canvasSlug && ( +
+
+
+

Collaborative Canvas

+ + Full Screen + +
+ +
+
+ )} + + {showCanvas && !trip.canvasSlug && ( +
+
+

No canvas linked yet.

+

+ A collaborative canvas will be created on rSpace when you're ready to plan visually with your travel partners. +

+
+
+ )} +
+
+
+ ); +} diff --git a/src/app/trips/new/page.tsx b/src/app/trips/new/page.tsx new file mode 100644 index 0000000..02aaee9 --- /dev/null +++ b/src/app/trips/new/page.tsx @@ -0,0 +1,90 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; +import { NLInput } from '@/components/NLInput'; +import { ParsedTripPreview } from '@/components/ParsedTripPreview'; +import { ParsedTrip } from '@/lib/types'; + +export default function NewTrip() { + const router = useRouter(); + const [parsed, setParsed] = useState(null); + const [rawInput, setRawInput] = useState(''); + const [creating, setCreating] = useState(false); + const [error, setError] = useState(null); + + const handleParsed = (data: ParsedTrip, raw: string) => { + setParsed(data); + setRawInput(raw); + }; + + const handleConfirm = async (finalParsed: ParsedTrip) => { + setCreating(true); + setError(null); + + try { + const res = await fetch('/api/trips', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ parsed: finalParsed, rawInput }), + }); + + if (!res.ok) { + const data = await res.json(); + throw new Error(data.error || 'Failed to create trip'); + } + + const trip = await res.json(); + router.push(`/trips/${trip.id}`); + } catch (err) { + setError(err instanceof Error ? err.message : 'Something went wrong'); + setCreating(false); + } + }; + + return ( +
+ {/* Nav */} + + + {/* Content */} +
+

Plan a New Trip

+

+ Describe your trip in natural language and we'll structure it for you. +

+ + {error && ( +
+ {error} +
+ )} + + {!parsed ? ( + + ) : ( + setParsed(null)} + loading={creating} + /> + )} +
+
+ ); +} diff --git a/src/app/trips/page.tsx b/src/app/trips/page.tsx new file mode 100644 index 0000000..4c54327 --- /dev/null +++ b/src/app/trips/page.tsx @@ -0,0 +1,126 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import Link from 'next/link'; +import { format } from 'date-fns'; + +interface TripSummary { + id: string; + title: string; + slug: string; + description: string | null; + startDate: string | null; + endDate: string | null; + budgetTotal: number | null; + budgetCurrency: string; + status: string; + destinations: { name: string; country: string | null }[]; + _count: { + itineraryItems: number; + bookings: number; + expenses: number; + packingItems: number; + }; + updatedAt: string; +} + +const STATUS_COLORS: Record = { + PLANNING: 'bg-teal-500/20 text-teal-300', + BOOKED: 'bg-blue-500/20 text-blue-300', + IN_PROGRESS: 'bg-amber-500/20 text-amber-300', + COMPLETED: 'bg-emerald-500/20 text-emerald-300', + CANCELLED: 'bg-slate-500/20 text-slate-400', +}; + +export default function TripsPage() { + const [trips, setTrips] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetch('/api/trips') + .then((res) => res.json()) + .then(setTrips) + .catch(console.error) + .finally(() => setLoading(false)); + }, []); + + return ( +
+ {/* Nav */} + + +
+

My Trips

+ + {loading ? ( +
+ + + + +
+ ) : trips.length === 0 ? ( +
+

No trips yet. Start planning your first adventure!

+ + Plan a Trip + +
+ ) : ( +
+ {trips.map((trip) => ( + +
+
+

{trip.title}

+ {trip.destinations.length > 0 && ( +

+ {trip.destinations.map((d) => d.name).join(' → ')} +

+ )} +
+ + {trip.status.replace('_', ' ')} + +
+ +
+ {trip.startDate && ( + {format(new Date(trip.startDate), 'MMM d, yyyy')} + )} + {trip.budgetTotal && ( + {trip.budgetCurrency} {trip.budgetTotal.toLocaleString()} + )} + {trip._count.itineraryItems} items + {trip._count.bookings} bookings +
+ + ))} +
+ )} +
+
+ ); +} diff --git a/src/components/CanvasEmbed.tsx b/src/components/CanvasEmbed.tsx new file mode 100644 index 0000000..6eb6f0e --- /dev/null +++ b/src/components/CanvasEmbed.tsx @@ -0,0 +1,35 @@ +'use client'; + +interface CanvasEmbedProps { + canvasSlug: string; + className?: string; +} + +export function CanvasEmbed({ canvasSlug, className = '' }: CanvasEmbedProps) { + const rspaceUrl = process.env.NEXT_PUBLIC_RSPACE_URL || 'https://rspace.online'; + const canvasUrl = `https://${canvasSlug}.rspace.online`; + + return ( +
+ {/* Toolbar */} + + + {/* Canvas iframe */} +