From 690ba30bbdf9a98b8b63b30905325724601fb232 Mon Sep 17 00:00:00 2001 From: Stewart Davis Date: Sun, 6 Jul 2025 18:37:02 +0300 Subject: [PATCH] Lots of stuff. deny and go back and update and etc --- CLAUDE.md | 60 ++++ package.json | 8 +- pnpm-lock.yaml | 434 +++++++++++------------ src/app/layout.tsx | 53 ++- src/app/login/form.tsx | 29 +- src/app/login/page.tsx | 28 +- src/app/page.tsx | 76 +++- src/app/promote/form.tsx | 40 ++- src/app/promote/page.tsx | 29 +- src/app/requests/[id]/edit/actions.ts | 66 ++++ src/app/requests/[id]/edit/edit-form.tsx | 108 ++++++ src/app/requests/[id]/edit/page.tsx | 141 ++++++++ src/app/requests/[id]/page.tsx | 222 +++++++++++- src/app/requests/new/actions.ts | 3 +- src/app/requests/new/form.tsx | 57 ++- src/app/requests/new/page.tsx | 28 +- src/app/requests/page.tsx | 60 +++- src/app/requests/request-item.tsx | 77 ++++ src/app/review/[id]/page.tsx | 272 ++++++++++++-- src/app/review/[id]/review-form.tsx | 137 +++++++ src/app/review/actions.ts | 84 +++++ src/app/review/page.tsx | 136 ++++++- src/app/signup/form.tsx | 35 +- src/app/signup/page.tsx | 28 +- src/components/pending.tsx | 58 ++- src/lib/firebase.ts | 13 +- src/styles/base.css | 167 ++++++++- src/utils/reviews.ts | 209 +++++++++++ 28 files changed, 2284 insertions(+), 374 deletions(-) create mode 100644 CLAUDE.md create mode 100644 src/app/requests/[id]/edit/actions.ts create mode 100644 src/app/requests/[id]/edit/edit-form.tsx create mode 100644 src/app/requests/[id]/edit/page.tsx create mode 100644 src/app/requests/request-item.tsx create mode 100644 src/app/review/[id]/review-form.tsx create mode 100644 src/app/review/actions.ts create mode 100644 src/utils/reviews.ts diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..68ebcf5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,60 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +- `pnpm dev` - Start development server with Turbopack for fast refresh +- `pnpm build` - Build the application for production +- `pnpm start` - Start the production server +- `pnpm lint` - Run ESLint to check code quality + +## Architecture Overview + +This is a Next.js 15 application built with TypeScript and Firebase, implementing a request management system with user authentication and admin functionality. + +### Firebase Integration + +- **Client-side**: Firebase v9 SDK (`src/lib/firebase.ts`) for Firestore database and client authentication +- **Server-side**: Firebase Admin SDK (`src/lib/admin.ts`) for server-side authentication and user management +- **Authentication**: Session-based auth using Firebase session cookies with server-side verification + +### Key Architecture Patterns + +**Server Actions**: All form submissions use Next.js server actions: + +- `src/app/requests/new/actions.ts` - Creates new requests +- `src/app/promote/actions.ts` - Promotes users to admin role + +**Authentication Flow**: + +- `src/utils/server.ts` contains `identify()` function for server-side user verification +- `hold()` function creates secure session cookies from Firebase tokens +- Admin access controlled via Firebase custom claims + +**Data Flow**: + +- Requests are stored in Firestore with user association via `uid` +- User identification happens server-side before database operations +- Form data processing uses `shape()` utility from `src/utils/client.ts` + +### Route Structure + +- `/` - Home page +- `/login` & `/signup` - Authentication pages +- `/requests` - User's request listing (protected) +- `/requests/new` - Create new request form (protected) +- `/requests/[id]` - Individual request details (protected) +- `/review` - Admin review interface (admin-only) +- `/promote` - User promotion to admin (admin-only) + +### Environment Configuration + +- Requires `GOOGLE_APPLICATION_CREDENTIALS` environment variable pointing to Firebase service account JSON +- Firebase client config is hardcoded in `src/lib/firebase.ts` + +### Styling + +- Uses Tailwind CSS v4 with PostCSS +- Base styles in `src/styles/base.css` +- Inter font loaded via Next.js font optimization diff --git a/package.json b/package.json index 81ffd87..59126cd 100644 --- a/package.json +++ b/package.json @@ -11,18 +11,18 @@ "firebase": "11.10.0", "firebase-admin": "13.4.0", "lucide-react": "0.525.0", - "next": "15.3.4", + "next": "15.3.5", "react": "19.1.0", "react-dom": "19.1.0" }, "devDependencies": { "@eslint/eslintrc": "3.3.1", "@tailwindcss/postcss": "4.1.11", - "@types/node": "24.0.8", + "@types/node": "24.0.10", "@types/react": "19.1.8", "@types/react-dom": "19.1.6", - "eslint": "9.30.0", - "eslint-config-next": "15.3.4", + "eslint": "9.30.1", + "eslint-config-next": "15.3.5", "prettier": "3.6.2", "prettier-plugin-tailwindcss": "0.6.13", "tailwindcss": "4.1.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2cbdded..85b6295 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: 0.525.0 version: 0.525.0(react@19.1.0) next: - specifier: 15.3.4 - version: 15.3.4(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: 15.3.5 + version: 15.3.5(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: specifier: 19.1.0 version: 19.1.0 @@ -34,8 +34,8 @@ importers: specifier: 4.1.11 version: 4.1.11 '@types/node': - specifier: 24.0.8 - version: 24.0.8 + specifier: 24.0.10 + version: 24.0.10 '@types/react': specifier: 19.1.8 version: 19.1.8 @@ -43,11 +43,11 @@ importers: specifier: 19.1.6 version: 19.1.6(@types/react@19.1.8) eslint: - specifier: 9.30.0 - version: 9.30.0(jiti@2.4.2) + specifier: 9.30.1 + version: 9.30.1(jiti@2.4.2) eslint-config-next: - specifier: 15.3.4 - version: 15.3.4(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3) + specifier: 15.3.5 + version: 15.3.5(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) prettier: specifier: 3.6.2 version: 3.6.2 @@ -110,8 +110,8 @@ packages: resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.30.0': - resolution: {integrity: sha512-Wzw3wQwPvc9sHM+NjakWTcPx11mbZyiYHuwWa/QfZ7cIRX7WK54PSk7bdyXDaoaopUcMatv1zaQvOAAO8hCdww==} + '@eslint/js@9.30.1': + resolution: {integrity: sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.6': @@ -508,18 +508,18 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} - '@jridgewell/gen-mapping@0.3.11': - resolution: {integrity: sha512-C512c1ytBTio4MrpWKlJpyFHT6+qfFL8SZ58zBzJ1OOzUEjHeF1BtjY2fH7n4x/g2OV/KiiMLAivOp1DXmiMMw==} + '@jridgewell/gen-mapping@0.3.12': + resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/sourcemap-codec@1.5.3': - resolution: {integrity: sha512-AiR5uKpFxP3PjO4R19kQGIMwxyRyPuXmKEEy301V1C0+1rVjS94EZQXf1QKZYN8Q0YM+estSPhmx5JwNftv6nw==} + '@jridgewell/sourcemap-codec@1.5.4': + resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} - '@jridgewell/trace-mapping@0.3.28': - resolution: {integrity: sha512-KNNHHwW3EIp4EDYOvYFGyIFfx36R2dNJYH4knnZlF8T5jdbD5Wx8xmSaQ2gP9URkJ04LGEtlcCtwArKcmFcwKw==} + '@jridgewell/trace-mapping@0.3.29': + resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} '@js-sdsl/ordered-map@4.4.2': resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} @@ -527,56 +527,56 @@ packages: '@napi-rs/wasm-runtime@0.2.11': resolution: {integrity: sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==} - '@next/env@15.3.4': - resolution: {integrity: sha512-ZkdYzBseS6UjYzz6ylVKPOK+//zLWvD6Ta+vpoye8cW11AjiQjGYVibF0xuvT4L0iJfAPfZLFidaEzAOywyOAQ==} + '@next/env@15.3.5': + resolution: {integrity: sha512-7g06v8BUVtN2njAX/r8gheoVffhiKFVt4nx74Tt6G4Hqw9HCLYQVx/GkH2qHvPtAHZaUNZ0VXAa0pQP6v1wk7g==} - '@next/eslint-plugin-next@15.3.4': - resolution: {integrity: sha512-lBxYdj7TI8phbJcLSAqDt57nIcobEign5NYIKCiy0hXQhrUbTqLqOaSDi568U6vFg4hJfBdZYsG4iP/uKhCqgg==} + '@next/eslint-plugin-next@15.3.5': + resolution: {integrity: sha512-BZwWPGfp9po/rAnJcwUBaM+yT/+yTWIkWdyDwc74G9jcfTrNrmsHe+hXHljV066YNdVs8cxROxX5IgMQGX190w==} - '@next/swc-darwin-arm64@15.3.4': - resolution: {integrity: sha512-z0qIYTONmPRbwHWvpyrFXJd5F9YWLCsw3Sjrzj2ZvMYy9NPQMPZ1NjOJh4ojr4oQzcGYwgJKfidzehaNa1BpEg==} + '@next/swc-darwin-arm64@15.3.5': + resolution: {integrity: sha512-lM/8tilIsqBq+2nq9kbTW19vfwFve0NR7MxfkuSUbRSgXlMQoJYg+31+++XwKVSXk4uT23G2eF/7BRIKdn8t8w==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.3.4': - resolution: {integrity: sha512-Z0FYJM8lritw5Wq+vpHYuCIzIlEMjewG2aRkc3Hi2rcbULknYL/xqfpBL23jQnCSrDUGAo/AEv0Z+s2bff9Zkw==} + '@next/swc-darwin-x64@15.3.5': + resolution: {integrity: sha512-WhwegPQJ5IfoUNZUVsI9TRAlKpjGVK0tpJTL6KeiC4cux9774NYE9Wu/iCfIkL/5J8rPAkqZpG7n+EfiAfidXA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.3.4': - resolution: {integrity: sha512-l8ZQOCCg7adwmsnFm8m5q9eIPAHdaB2F3cxhufYtVo84pymwKuWfpYTKcUiFcutJdp9xGHC+F1Uq3xnFU1B/7g==} + '@next/swc-linux-arm64-gnu@15.3.5': + resolution: {integrity: sha512-LVD6uMOZ7XePg3KWYdGuzuvVboxujGjbcuP2jsPAN3MnLdLoZUXKRc6ixxfs03RH7qBdEHCZjyLP/jBdCJVRJQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.3.4': - resolution: {integrity: sha512-wFyZ7X470YJQtpKot4xCY3gpdn8lE9nTlldG07/kJYexCUpX1piX+MBfZdvulo+t1yADFVEuzFfVHfklfEx8kw==} + '@next/swc-linux-arm64-musl@15.3.5': + resolution: {integrity: sha512-k8aVScYZ++BnS2P69ClK7v4nOu702jcF9AIHKu6llhHEtBSmM2zkPGl9yoqbSU/657IIIb0QHpdxEr0iW9z53A==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.3.4': - resolution: {integrity: sha512-gEbH9rv9o7I12qPyvZNVTyP/PWKqOp8clvnoYZQiX800KkqsaJZuOXkWgMa7ANCCh/oEN2ZQheh3yH8/kWPSEg==} + '@next/swc-linux-x64-gnu@15.3.5': + resolution: {integrity: sha512-2xYU0DI9DGN/bAHzVwADid22ba5d/xrbrQlr2U+/Q5WkFUzeL0TDR963BdrtLS/4bMmKZGptLeg6282H/S2i8A==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.3.4': - resolution: {integrity: sha512-Cf8sr0ufuC/nu/yQ76AnarbSAXcwG/wj+1xFPNbyNo8ltA6kw5d5YqO8kQuwVIxk13SBdtgXrNyom3ZosHAy4A==} + '@next/swc-linux-x64-musl@15.3.5': + resolution: {integrity: sha512-TRYIqAGf1KCbuAB0gjhdn5Ytd8fV+wJSM2Nh2is/xEqR8PZHxfQuaiNhoF50XfY90sNpaRMaGhF6E+qjV1b9Tg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.3.4': - resolution: {integrity: sha512-ay5+qADDN3rwRbRpEhTOreOn1OyJIXS60tg9WMYTWCy3fB6rGoyjLVxc4dR9PYjEdR2iDYsaF5h03NA+XuYPQQ==} + '@next/swc-win32-arm64-msvc@15.3.5': + resolution: {integrity: sha512-h04/7iMEUSMY6fDGCvdanKqlO1qYvzNxntZlCzfE8i5P0uqzVQWQquU1TIhlz0VqGQGXLrFDuTJVONpqGqjGKQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.3.4': - resolution: {integrity: sha512-4kDt31Bc9DGyYs41FTL1/kNpDeHyha2TC0j5sRRoKCyrhNcfZ/nRQkAUlF27mETwm8QyHqIjHJitfcza2Iykfg==} + '@next/swc-win32-x64-msvc@15.3.5': + resolution: {integrity: sha512-5fhH6fccXxnX2KhllnGhkYMndhOiLOLEiVGYjP2nizqeGWkN10sA9taATlXwake2E2XMvYZjjz0Uj7T0y+z1yw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -777,11 +777,11 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@22.15.34': - resolution: {integrity: sha512-8Y6E5WUupYy1Dd0II32BsWAx5MWdcnRd8L84Oys3veg1YrYtNtzgO4CFhiBg6MDSjk7Ay36HYOnU7/tuOzIzcw==} + '@types/node@22.16.0': + resolution: {integrity: sha512-B2egV9wALML1JCpv3VQoQ+yesQKAmNMBIAY7OteVrikcOcAkWm+dGL6qpeCktPjAv6N1JLnhbNiqS35UpFyBsQ==} - '@types/node@24.0.8': - resolution: {integrity: sha512-WytNrFSgWO/esSH9NbpWUfTMGQwCGIKfCmNlmFDNiI5gGhgMmEA+V1AEvKLeBNvvtBnailJtkrEa2OIISwrVAA==} + '@types/node@24.0.10': + resolution: {integrity: sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==} '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} @@ -868,98 +868,98 @@ packages: resolution: {integrity: sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@unrs/resolver-binding-android-arm-eabi@1.9.2': - resolution: {integrity: sha512-tS+lqTU3N0kkthU+rYp0spAYq15DU8ld9kXkaKg9sbQqJNF+WPMuNHZQGCgdxrUOEO0j22RKMwRVhF1HTl+X8A==} + '@unrs/resolver-binding-android-arm-eabi@1.10.1': + resolution: {integrity: sha512-zohDKXT1Ok0yhbVGff4YAg9HUs5ietG5GpvJBPFSApZnGe7uf2cd26DRhKZbn0Be6xHUZrSzP+RAgMmzyc71EA==} cpu: [arm] os: [android] - '@unrs/resolver-binding-android-arm64@1.9.2': - resolution: {integrity: sha512-MffGiZULa/KmkNjHeuuflLVqfhqLv1vZLm8lWIyeADvlElJ/GLSOkoUX+5jf4/EGtfwrNFcEaB8BRas03KT0/Q==} + '@unrs/resolver-binding-android-arm64@1.10.1': + resolution: {integrity: sha512-tAN6k5UrTd4nicpA7s2PbjR/jagpDzAmvXFjbpTazUe5FRsFxVcBlS1F5Lzp5jtWU6bdiqRhSvd4X8rdpCffeA==} cpu: [arm64] os: [android] - '@unrs/resolver-binding-darwin-arm64@1.9.2': - resolution: {integrity: sha512-dzJYK5rohS1sYl1DHdJ3mwfwClJj5BClQnQSyAgEfggbUwA9RlROQSSbKBLqrGfsiC/VyrDPtbO8hh56fnkbsQ==} + '@unrs/resolver-binding-darwin-arm64@1.10.1': + resolution: {integrity: sha512-+FCsag8WkauI4dQ50XumCXdfvDCZEpMUnvZDsKMxfOisnEklpDFXc6ThY0WqybBYZbiwR5tWcFaZmI0G6b4vrg==} cpu: [arm64] os: [darwin] - '@unrs/resolver-binding-darwin-x64@1.9.2': - resolution: {integrity: sha512-gaIMWK+CWtXcg9gUyznkdV54LzQ90S3X3dn8zlh+QR5Xy7Y+Efqw4Rs4im61K1juy4YNb67vmJsCDAGOnIeffQ==} + '@unrs/resolver-binding-darwin-x64@1.10.1': + resolution: {integrity: sha512-qYKGGm5wk71ONcXTMZ0+J11qQeOAPz3nw6VtqrBUUELRyXFyvK8cHhHsLBFR4GHnilc2pgY1HTB2TvdW9wO26Q==} cpu: [x64] os: [darwin] - '@unrs/resolver-binding-freebsd-x64@1.9.2': - resolution: {integrity: sha512-S7QpkMbVoVJb0xwHFwujnwCAEDe/596xqY603rpi/ioTn9VDgBHnCCxh+UFrr5yxuMH+dliHfjwCZJXOPJGPnw==} + '@unrs/resolver-binding-freebsd-x64@1.10.1': + resolution: {integrity: sha512-hOHMAhbvIQ63gkpgeNsXcWPSyvXH7ZEyeg254hY0Lp/hX8NdW+FsUWq73g9946Pc/BrcVI/I3C1cmZ4RCX9bNw==} cpu: [x64] os: [freebsd] - '@unrs/resolver-binding-linux-arm-gnueabihf@1.9.2': - resolution: {integrity: sha512-+XPUMCuCCI80I46nCDFbGum0ZODP5NWGiwS3Pj8fOgsG5/ctz+/zzuBlq/WmGa+EjWZdue6CF0aWWNv84sE1uw==} + '@unrs/resolver-binding-linux-arm-gnueabihf@1.10.1': + resolution: {integrity: sha512-6ds7+zzHJgTDmpe0gmFcOTvSUhG5oZukkt+cCsSb3k4Uiz2yEQB4iCRITX2hBwSW+p8gAieAfecITjgqCkswXw==} cpu: [arm] os: [linux] - '@unrs/resolver-binding-linux-arm-musleabihf@1.9.2': - resolution: {integrity: sha512-sqvUyAd1JUpwbz33Ce2tuTLJKM+ucSsYpPGl2vuFwZnEIg0CmdxiZ01MHQ3j6ExuRqEDUCy8yvkDKvjYFPb8Zg==} + '@unrs/resolver-binding-linux-arm-musleabihf@1.10.1': + resolution: {integrity: sha512-P7A0G2/jW00diNJyFeq4W9/nxovD62Ay8CMP4UK9OymC7qO7rG1a8Upad68/bdfpIOn7KSp7Aj/6lEW3yyznAA==} cpu: [arm] os: [linux] - '@unrs/resolver-binding-linux-arm64-gnu@1.9.2': - resolution: {integrity: sha512-UYA0MA8ajkEDCFRQdng/FVx3F6szBvk3EPnkTTQuuO9lV1kPGuTB+V9TmbDxy5ikaEgyWKxa4CI3ySjklZ9lFA==} + '@unrs/resolver-binding-linux-arm64-gnu@1.10.1': + resolution: {integrity: sha512-Cg6xzdkrpltcTPO4At+A79zkC7gPDQIgosJmVV8M104ImB6KZi1MrNXgDYIAfkhUYjPzjNooEDFRAwwPadS7ZA==} cpu: [arm64] os: [linux] - '@unrs/resolver-binding-linux-arm64-musl@1.9.2': - resolution: {integrity: sha512-P/CO3ODU9YJIHFqAkHbquKtFst0COxdphc8TKGL5yCX75GOiVpGqd1d15ahpqu8xXVsqP4MGFP2C3LRZnnL5MA==} + '@unrs/resolver-binding-linux-arm64-musl@1.10.1': + resolution: {integrity: sha512-aNeg99bVkXa4lt+oZbjNRPC8ZpjJTKxijg/wILrJdzNyAymO2UC/HUK1UfDjt6T7U5p/mK24T3CYOi3/+YEQSA==} cpu: [arm64] os: [linux] - '@unrs/resolver-binding-linux-ppc64-gnu@1.9.2': - resolution: {integrity: sha512-uKStFlOELBxBum2s1hODPtgJhY4NxYJE9pAeyBgNEzHgTqTiVBPjfTlPFJkfxyTjQEuxZbbJlJnMCrRgD7ubzw==} + '@unrs/resolver-binding-linux-ppc64-gnu@1.10.1': + resolution: {integrity: sha512-ylz5ojeXrkPrtnzVhpCO+YegG63/aKhkoTlY8PfMfBfLaUG8v6m6iqrL7sBUKdVBgOB4kSTUPt9efQdA/Y3Z/w==} cpu: [ppc64] os: [linux] - '@unrs/resolver-binding-linux-riscv64-gnu@1.9.2': - resolution: {integrity: sha512-LkbNnZlhINfY9gK30AHs26IIVEZ9PEl9qOScYdmY2o81imJYI4IMnJiW0vJVtXaDHvBvxeAgEy5CflwJFIl3tQ==} + '@unrs/resolver-binding-linux-riscv64-gnu@1.10.1': + resolution: {integrity: sha512-xcWyhmJfXXOxK7lvE4+rLwBq+on83svlc0AIypfe6x4sMJR+S4oD7n9OynaQShfj2SufPw2KJAotnsNb+4nN2g==} cpu: [riscv64] os: [linux] - '@unrs/resolver-binding-linux-riscv64-musl@1.9.2': - resolution: {integrity: sha512-vI+e6FzLyZHSLFNomPi+nT+qUWN4YSj8pFtQZSFTtmgFoxqB6NyjxSjAxEC1m93qn6hUXhIsh8WMp+fGgxCoRg==} + '@unrs/resolver-binding-linux-riscv64-musl@1.10.1': + resolution: {integrity: sha512-mW9JZAdOCyorgi1eLJr4gX7xS67WNG9XNPYj5P8VuttK72XNsmdw9yhOO4tDANMgiLXFiSFaiL1gEpoNtRPw/A==} cpu: [riscv64] os: [linux] - '@unrs/resolver-binding-linux-s390x-gnu@1.9.2': - resolution: {integrity: sha512-sSO4AlAYhSM2RAzBsRpahcJB1msc6uYLAtP6pesPbZtptF8OU/CbCPhSRW6cnYOGuVmEmWVW5xVboAqCnWTeHQ==} + '@unrs/resolver-binding-linux-s390x-gnu@1.10.1': + resolution: {integrity: sha512-NZGKhBy6xkJ0k09cWNZz4DnhBcGlhDd3W+j7EYoNvf5TSwj2K6kbmfqTWITEgkvjsMUjm1wsrc4IJaH6VtjyHQ==} cpu: [s390x] os: [linux] - '@unrs/resolver-binding-linux-x64-gnu@1.9.2': - resolution: {integrity: sha512-jkSkwch0uPFva20Mdu8orbQjv2A3G88NExTN2oPTI1AJ+7mZfYW3cDCTyoH6OnctBKbBVeJCEqh0U02lTkqD5w==} + '@unrs/resolver-binding-linux-x64-gnu@1.10.1': + resolution: {integrity: sha512-VsjgckJ0gNMw7p0d8In6uPYr+s0p16yrT2rvG4v2jUpEMYkpnfnCiALa9SWshbvlGjKQ98Q2x19agm3iFk8w8Q==} cpu: [x64] os: [linux] - '@unrs/resolver-binding-linux-x64-musl@1.9.2': - resolution: {integrity: sha512-Uk64NoiTpQbkpl+bXsbeyOPRpUoMdcUqa+hDC1KhMW7aN1lfW8PBlBH4mJ3n3Y47dYE8qi0XTxy1mBACruYBaw==} + '@unrs/resolver-binding-linux-x64-musl@1.10.1': + resolution: {integrity: sha512-idMnajMeejnaFi0Mx9UTLSYFDAOTfAEP7VjXNgxKApso3Eu2Njs0p2V95nNIyFi4oQVGFmIuCkoznAXtF/Zbmw==} cpu: [x64] os: [linux] - '@unrs/resolver-binding-wasm32-wasi@1.9.2': - resolution: {integrity: sha512-EpBGwkcjDicjR/ybC0g8wO5adPNdVuMrNalVgYcWi+gYtC1XYNuxe3rufcO7dA76OHGeVabcO6cSkPJKVcbCXQ==} + '@unrs/resolver-binding-wasm32-wasi@1.10.1': + resolution: {integrity: sha512-7jyhjIRNFjzlr8x5pth6Oi9hv3a7ubcVYm2GBFinkBQKcFhw4nIs5BtauSNtDW1dPIGrxF0ciynCZqzxMrYMsg==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@unrs/resolver-binding-win32-arm64-msvc@1.9.2': - resolution: {integrity: sha512-EdFbGn7o1SxGmN6aZw9wAkehZJetFPao0VGZ9OMBwKx6TkvDuj6cNeLimF/Psi6ts9lMOe+Dt6z19fZQ9Ye2fw==} + '@unrs/resolver-binding-win32-arm64-msvc@1.10.1': + resolution: {integrity: sha512-TY79+N+Gkoo7E99K+zmsKNeiuNJYlclZJtKqsHSls8We2iGhgxtletVsiBYie93MSTDRDMI8pkBZJlIJSZPrdA==} cpu: [arm64] os: [win32] - '@unrs/resolver-binding-win32-ia32-msvc@1.9.2': - resolution: {integrity: sha512-JY9hi1p7AG+5c/dMU8o2kWemM8I6VZxfGwn1GCtf3c5i+IKcMo2NQ8OjZ4Z3/itvY/Si3K10jOBQn7qsD/whUA==} + '@unrs/resolver-binding-win32-ia32-msvc@1.10.1': + resolution: {integrity: sha512-BAJN5PEPlEV+1m8+PCtFoKm3LQ1P57B4Z+0+efU0NzmCaGk7pUaOxuPgl+m3eufVeeNBKiPDltG0sSB9qEfCxw==} cpu: [ia32] os: [win32] - '@unrs/resolver-binding-win32-x64-msvc@1.9.2': - resolution: {integrity: sha512-ryoo+EB19lMxAd80ln9BVf8pdOAxLb97amrQ3SFN9OCRn/5M5wvwDgAe4i8ZjhpbiHoDeP8yavcTEnpKBo7lZg==} + '@unrs/resolver-binding-win32-x64-msvc@1.10.1': + resolution: {integrity: sha512-2v3erKKmmCyIVvvhI2nF15qEbdBpISTq44m9pyd5gfIJB1PN94oePTLWEd82XUbIbvKhv76xTSeUQSCOGesLeg==} cpu: [x64] os: [win32] @@ -1270,8 +1270,8 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-config-next@15.3.4: - resolution: {integrity: sha512-WqeumCq57QcTP2lYlV6BRUySfGiBYEXlQ1L0mQ+u4N4X4ZhUVSSQ52WtjqHv60pJ6dD7jn+YZc0d1/ZSsxccvg==} + eslint-config-next@15.3.5: + resolution: {integrity: sha512-oQdvnIgP68wh2RlR3MdQpvaJ94R6qEFl+lnu8ZKxPj5fsAHrSF/HlAOZcsimLw3DT6bnEQIUdbZC2Ab6sWyptg==} peerDependencies: eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 typescript: '>=3.3.1' @@ -1356,8 +1356,8 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.30.0: - resolution: {integrity: sha512-iN/SiPxmQu6EVkf+m1qpBxzUhE12YqFLOSySuOyVLJLEF9nzTf+h/1AJYc1JWzCnktggeNrjvQGLngDzXirU6g==} + eslint@9.30.1: + resolution: {integrity: sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -1991,16 +1991,16 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - napi-postinstall@0.2.5: - resolution: {integrity: sha512-kmsgUvCRIJohHjbZ3V8avP0I1Pekw329MVAMDzVxsrkjgdnqiwvMX5XwR+hWV66vsAtZ+iM+fVnq8RTQawUmCQ==} + napi-postinstall@0.3.0: + resolution: {integrity: sha512-M7NqKyhODKV1gRLdkwE7pDsZP2/SC2a2vHkOYh9MCpKMbWVfyVfUw5MaH83Fv6XMjxr5jryUp3IDDL9rlxsTeA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} hasBin: true natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - next@15.3.4: - resolution: {integrity: sha512-mHKd50C+mCjam/gcnwqL1T1vPx/XQNFlXqFIVdgQdVAFY9iIQtY0IfaVflEYzKiqjeA7B0cYYMaCrmAYFjs4rA==} + next@15.3.5: + resolution: {integrity: sha512-RkazLBMMDJSJ4XZQ81kolSpwiCt907l0xcgcpF4xC2Vml6QVcPNXW0NQRwQ80FFtSn7UM52XN0anaw8TEJXaiw==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: @@ -2500,8 +2500,8 @@ packages: undici-types@7.8.0: resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} - unrs-resolver@1.9.2: - resolution: {integrity: sha512-VUyWiTNQD7itdiMuJy+EuLEErLj3uwX/EpHQF8EOf33Dq3Ju6VW1GXm+swk6+1h7a49uv9fKZ+dft9jU7esdLA==} + unrs-resolver@1.10.1: + resolution: {integrity: sha512-EFrL7Hw4kmhZdwWO3dwwFJo6hO3FXuQ6Bg8BK/faHZ9m1YxqBS31BNSTxklIQkxK/4LlV8zTYnPsIRLBzTzjCA==} uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -2599,8 +2599,8 @@ snapshots: '@ampproject/remapping@2.3.0': dependencies: - '@jridgewell/gen-mapping': 0.3.11 - '@jridgewell/trace-mapping': 0.3.28 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 '@emnapi/core@1.4.3': dependencies: @@ -2618,9 +2618,9 @@ snapshots: tslib: 2.8.1 optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@9.30.0(jiti@2.4.2))': + '@eslint-community/eslint-utils@4.7.0(eslint@9.30.1(jiti@2.4.2))': dependencies: - eslint: 9.30.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -2657,7 +2657,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.30.0': {} + '@eslint/js@9.30.1': {} '@eslint/object-schema@2.1.6': {} @@ -3041,7 +3041,7 @@ snapshots: '@grpc/grpc-js@1.9.15': dependencies: '@grpc/proto-loader': 0.7.15 - '@types/node': 24.0.8 + '@types/node': 24.0.10 '@grpc/proto-loader@0.7.15': dependencies: @@ -3148,19 +3148,19 @@ snapshots: dependencies: minipass: 7.1.2 - '@jridgewell/gen-mapping@0.3.11': + '@jridgewell/gen-mapping@0.3.12': dependencies: - '@jridgewell/sourcemap-codec': 1.5.3 - '@jridgewell/trace-mapping': 0.3.28 + '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/trace-mapping': 0.3.29 '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/sourcemap-codec@1.5.3': {} + '@jridgewell/sourcemap-codec@1.5.4': {} - '@jridgewell/trace-mapping@0.3.28': + '@jridgewell/trace-mapping@0.3.29': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.3 + '@jridgewell/sourcemap-codec': 1.5.4 '@js-sdsl/ordered-map@4.4.2': optional: true @@ -3172,34 +3172,34 @@ snapshots: '@tybys/wasm-util': 0.9.0 optional: true - '@next/env@15.3.4': {} + '@next/env@15.3.5': {} - '@next/eslint-plugin-next@15.3.4': + '@next/eslint-plugin-next@15.3.5': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@15.3.4': + '@next/swc-darwin-arm64@15.3.5': optional: true - '@next/swc-darwin-x64@15.3.4': + '@next/swc-darwin-x64@15.3.5': optional: true - '@next/swc-linux-arm64-gnu@15.3.4': + '@next/swc-linux-arm64-gnu@15.3.5': optional: true - '@next/swc-linux-arm64-musl@15.3.4': + '@next/swc-linux-arm64-musl@15.3.5': optional: true - '@next/swc-linux-x64-gnu@15.3.4': + '@next/swc-linux-x64-gnu@15.3.5': optional: true - '@next/swc-linux-x64-musl@15.3.4': + '@next/swc-linux-x64-musl@15.3.5': optional: true - '@next/swc-win32-arm64-msvc@15.3.4': + '@next/swc-win32-arm64-msvc@15.3.5': optional: true - '@next/swc-win32-x64-msvc@15.3.4': + '@next/swc-win32-x64-msvc@15.3.5': optional: true '@nodelib/fs.scandir@2.1.5': @@ -3335,20 +3335,20 @@ snapshots: '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 24.0.8 + '@types/node': 24.0.10 '@types/caseless@0.12.5': optional: true '@types/connect@3.4.38': dependencies: - '@types/node': 24.0.8 + '@types/node': 24.0.10 '@types/estree@1.0.8': {} '@types/express-serve-static-core@4.19.6': dependencies: - '@types/node': 24.0.8 + '@types/node': 24.0.10 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 0.17.5 @@ -3369,7 +3369,7 @@ snapshots: '@types/jsonwebtoken@9.0.10': dependencies: '@types/ms': 2.1.0 - '@types/node': 24.0.8 + '@types/node': 24.0.10 '@types/long@4.0.2': optional: true @@ -3378,11 +3378,11 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@22.15.34': + '@types/node@22.16.0': dependencies: undici-types: 6.21.0 - '@types/node@24.0.8': + '@types/node@24.0.10': dependencies: undici-types: 7.8.0 @@ -3401,7 +3401,7 @@ snapshots: '@types/request@2.48.12': dependencies: '@types/caseless': 0.12.5 - '@types/node': 24.0.8 + '@types/node': 24.0.10 '@types/tough-cookie': 4.0.5 form-data: 2.5.3 optional: true @@ -3409,26 +3409,26 @@ snapshots: '@types/send@0.17.5': dependencies: '@types/mime': 1.3.5 - '@types/node': 24.0.8 + '@types/node': 24.0.10 '@types/serve-static@1.15.8': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 24.0.8 + '@types/node': 24.0.10 '@types/send': 0.17.5 '@types/tough-cookie@4.0.5': optional: true - '@typescript-eslint/eslint-plugin@8.35.1(@typescript-eslint/parser@8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.35.1(@typescript-eslint/parser@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.35.1 - '@typescript-eslint/type-utils': 8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/type-utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.35.1 - eslint: 9.30.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -3437,14 +3437,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/parser@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@typescript-eslint/scope-manager': 8.35.1 '@typescript-eslint/types': 8.35.1 '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.35.1 debug: 4.4.1 - eslint: 9.30.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -3467,12 +3467,12 @@ snapshots: dependencies: typescript: 5.8.3 - '@typescript-eslint/type-utils@8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) - '@typescript-eslint/utils': 8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) debug: 4.4.1 - eslint: 9.30.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: @@ -3496,13 +3496,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/utils@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2)) '@typescript-eslint/scope-manager': 8.35.1 '@typescript-eslint/types': 8.35.1 '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) - eslint: 9.30.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -3512,63 +3512,63 @@ snapshots: '@typescript-eslint/types': 8.35.1 eslint-visitor-keys: 4.2.1 - '@unrs/resolver-binding-android-arm-eabi@1.9.2': + '@unrs/resolver-binding-android-arm-eabi@1.10.1': optional: true - '@unrs/resolver-binding-android-arm64@1.9.2': + '@unrs/resolver-binding-android-arm64@1.10.1': optional: true - '@unrs/resolver-binding-darwin-arm64@1.9.2': + '@unrs/resolver-binding-darwin-arm64@1.10.1': optional: true - '@unrs/resolver-binding-darwin-x64@1.9.2': + '@unrs/resolver-binding-darwin-x64@1.10.1': optional: true - '@unrs/resolver-binding-freebsd-x64@1.9.2': + '@unrs/resolver-binding-freebsd-x64@1.10.1': optional: true - '@unrs/resolver-binding-linux-arm-gnueabihf@1.9.2': + '@unrs/resolver-binding-linux-arm-gnueabihf@1.10.1': optional: true - '@unrs/resolver-binding-linux-arm-musleabihf@1.9.2': + '@unrs/resolver-binding-linux-arm-musleabihf@1.10.1': optional: true - '@unrs/resolver-binding-linux-arm64-gnu@1.9.2': + '@unrs/resolver-binding-linux-arm64-gnu@1.10.1': optional: true - '@unrs/resolver-binding-linux-arm64-musl@1.9.2': + '@unrs/resolver-binding-linux-arm64-musl@1.10.1': optional: true - '@unrs/resolver-binding-linux-ppc64-gnu@1.9.2': + '@unrs/resolver-binding-linux-ppc64-gnu@1.10.1': optional: true - '@unrs/resolver-binding-linux-riscv64-gnu@1.9.2': + '@unrs/resolver-binding-linux-riscv64-gnu@1.10.1': optional: true - '@unrs/resolver-binding-linux-riscv64-musl@1.9.2': + '@unrs/resolver-binding-linux-riscv64-musl@1.10.1': optional: true - '@unrs/resolver-binding-linux-s390x-gnu@1.9.2': + '@unrs/resolver-binding-linux-s390x-gnu@1.10.1': optional: true - '@unrs/resolver-binding-linux-x64-gnu@1.9.2': + '@unrs/resolver-binding-linux-x64-gnu@1.10.1': optional: true - '@unrs/resolver-binding-linux-x64-musl@1.9.2': + '@unrs/resolver-binding-linux-x64-musl@1.10.1': optional: true - '@unrs/resolver-binding-wasm32-wasi@1.9.2': + '@unrs/resolver-binding-wasm32-wasi@1.10.1': dependencies: '@napi-rs/wasm-runtime': 0.2.11 optional: true - '@unrs/resolver-binding-win32-arm64-msvc@1.9.2': + '@unrs/resolver-binding-win32-arm64-msvc@1.10.1': optional: true - '@unrs/resolver-binding-win32-ia32-msvc@1.9.2': + '@unrs/resolver-binding-win32-ia32-msvc@1.10.1': optional: true - '@unrs/resolver-binding-win32-x64-msvc@1.9.2': + '@unrs/resolver-binding-win32-x64-msvc@1.10.1': optional: true abort-controller@3.0.0: @@ -3980,19 +3980,19 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-next@15.3.4(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3): + eslint-config-next@15.3.5(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3): dependencies: - '@next/eslint-plugin-next': 15.3.4 + '@next/eslint-plugin-next': 15.3.5 '@rushstack/eslint-patch': 1.12.0 - '@typescript-eslint/eslint-plugin': 8.35.1(@typescript-eslint/parser@8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/parser': 8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.30.0(jiti@2.4.2) + '@typescript-eslint/eslint-plugin': 8.35.1(@typescript-eslint/parser@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.30.1(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.30.0(jiti@2.4.2)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.30.0(jiti@2.4.2)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.30.0(jiti@2.4.2)) - eslint-plugin-react: 7.37.5(eslint@9.30.0(jiti@2.4.2)) - eslint-plugin-react-hooks: 5.2.0(eslint@9.30.0(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.30.1(jiti@2.4.2)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.30.1(jiti@2.4.2)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.30.1(jiti@2.4.2)) + eslint-plugin-react: 7.37.5(eslint@9.30.1(jiti@2.4.2)) + eslint-plugin-react-hooks: 5.2.0(eslint@9.30.1(jiti@2.4.2)) optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: @@ -4008,33 +4008,33 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.30.0(jiti@2.4.2)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.30.1(jiti@2.4.2)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1 - eslint: 9.30.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) get-tsconfig: 4.10.1 is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.14 - unrs-resolver: 1.9.2 + unrs-resolver: 1.10.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.30.0(jiti@2.4.2)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.30.1(jiti@2.4.2)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.30.0(jiti@2.4.2)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.30.1(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.30.0(jiti@2.4.2) + '@typescript-eslint/parser': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.30.1(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.30.0(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.30.1(jiti@2.4.2)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.30.0(jiti@2.4.2)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.30.1(jiti@2.4.2)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -4043,9 +4043,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.30.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.30.0(jiti@2.4.2)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.30.1(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -4057,13 +4057,13 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.30.0(jiti@2.4.2)): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.30.1(jiti@2.4.2)): dependencies: aria-query: 5.3.2 array-includes: 3.1.9 @@ -4073,7 +4073,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.30.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -4082,11 +4082,11 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-hooks@5.2.0(eslint@9.30.0(jiti@2.4.2)): + eslint-plugin-react-hooks@5.2.0(eslint@9.30.1(jiti@2.4.2)): dependencies: - eslint: 9.30.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) - eslint-plugin-react@7.37.5(eslint@9.30.0(jiti@2.4.2)): + eslint-plugin-react@7.37.5(eslint@9.30.1(jiti@2.4.2)): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 @@ -4094,7 +4094,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.1 - eslint: 9.30.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -4117,15 +4117,15 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.30.0(jiti@2.4.2): + eslint@9.30.1(jiti@2.4.2): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.21.0 '@eslint/config-helpers': 0.3.0 '@eslint/core': 0.14.0 '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.30.0 + '@eslint/js': 9.30.1 '@eslint/plugin-kit': 0.3.3 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 @@ -4241,7 +4241,7 @@ snapshots: '@fastify/busboy': 3.1.1 '@firebase/database-compat': 2.0.11 '@firebase/database-types': 1.0.15 - '@types/node': 22.15.34 + '@types/node': 22.16.0 farmhash-modern: 1.1.0 google-auth-library: 9.15.1 jsonwebtoken: 9.0.2 @@ -4824,7 +4824,7 @@ snapshots: magic-string@0.30.17: dependencies: - '@jridgewell/sourcemap-codec': 1.5.3 + '@jridgewell/sourcemap-codec': 1.5.4 math-intrinsics@1.1.0: {} @@ -4868,13 +4868,13 @@ snapshots: nanoid@3.3.11: {} - napi-postinstall@0.2.5: {} + napi-postinstall@0.3.0: {} natural-compare@1.4.0: {} - next@15.3.4(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + next@15.3.5(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - '@next/env': 15.3.4 + '@next/env': 15.3.5 '@swc/counter': 0.1.3 '@swc/helpers': 0.5.15 busboy: 1.6.0 @@ -4884,14 +4884,14 @@ snapshots: react-dom: 19.1.0(react@19.1.0) styled-jsx: 5.1.6(react@19.1.0) optionalDependencies: - '@next/swc-darwin-arm64': 15.3.4 - '@next/swc-darwin-x64': 15.3.4 - '@next/swc-linux-arm64-gnu': 15.3.4 - '@next/swc-linux-arm64-musl': 15.3.4 - '@next/swc-linux-x64-gnu': 15.3.4 - '@next/swc-linux-x64-musl': 15.3.4 - '@next/swc-win32-arm64-msvc': 15.3.4 - '@next/swc-win32-x64-msvc': 15.3.4 + '@next/swc-darwin-arm64': 15.3.5 + '@next/swc-darwin-x64': 15.3.5 + '@next/swc-linux-arm64-gnu': 15.3.5 + '@next/swc-linux-arm64-musl': 15.3.5 + '@next/swc-linux-x64-gnu': 15.3.5 + '@next/swc-linux-x64-musl': 15.3.5 + '@next/swc-win32-arm64-msvc': 15.3.5 + '@next/swc-win32-x64-msvc': 15.3.5 '@opentelemetry/api': 1.9.0 sharp: 0.34.2 transitivePeerDependencies: @@ -5038,7 +5038,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 24.0.8 + '@types/node': 24.0.10 long: 5.3.2 punycode@2.3.1: {} @@ -5439,29 +5439,29 @@ snapshots: undici-types@7.8.0: {} - unrs-resolver@1.9.2: + unrs-resolver@1.10.1: dependencies: - napi-postinstall: 0.2.5 + napi-postinstall: 0.3.0 optionalDependencies: - '@unrs/resolver-binding-android-arm-eabi': 1.9.2 - '@unrs/resolver-binding-android-arm64': 1.9.2 - '@unrs/resolver-binding-darwin-arm64': 1.9.2 - '@unrs/resolver-binding-darwin-x64': 1.9.2 - '@unrs/resolver-binding-freebsd-x64': 1.9.2 - '@unrs/resolver-binding-linux-arm-gnueabihf': 1.9.2 - '@unrs/resolver-binding-linux-arm-musleabihf': 1.9.2 - '@unrs/resolver-binding-linux-arm64-gnu': 1.9.2 - '@unrs/resolver-binding-linux-arm64-musl': 1.9.2 - '@unrs/resolver-binding-linux-ppc64-gnu': 1.9.2 - '@unrs/resolver-binding-linux-riscv64-gnu': 1.9.2 - '@unrs/resolver-binding-linux-riscv64-musl': 1.9.2 - '@unrs/resolver-binding-linux-s390x-gnu': 1.9.2 - '@unrs/resolver-binding-linux-x64-gnu': 1.9.2 - '@unrs/resolver-binding-linux-x64-musl': 1.9.2 - '@unrs/resolver-binding-wasm32-wasi': 1.9.2 - '@unrs/resolver-binding-win32-arm64-msvc': 1.9.2 - '@unrs/resolver-binding-win32-ia32-msvc': 1.9.2 - '@unrs/resolver-binding-win32-x64-msvc': 1.9.2 + '@unrs/resolver-binding-android-arm-eabi': 1.10.1 + '@unrs/resolver-binding-android-arm64': 1.10.1 + '@unrs/resolver-binding-darwin-arm64': 1.10.1 + '@unrs/resolver-binding-darwin-x64': 1.10.1 + '@unrs/resolver-binding-freebsd-x64': 1.10.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.10.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.10.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.10.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.10.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.10.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.10.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.10.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.10.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.10.1 + '@unrs/resolver-binding-linux-x64-musl': 1.10.1 + '@unrs/resolver-binding-wasm32-wasi': 1.10.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.10.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.10.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.10.1 uri-js@4.4.1: dependencies: diff --git a/src/app/layout.tsx b/src/app/layout.tsx index bd76dba..b1aefd7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,6 +2,7 @@ import "@/styles/base.css" import { Metadata } from "next" import { Inter } from "next/font/google" import { ReactNode } from "react" +import Link from "next/link" const inter = Inter({ subsets: ["latin"] }) @@ -13,9 +14,55 @@ export default function Layout({ children }: { children: ReactNode }) { return ( -
-
{children}
- +
+
+
+
+ + Tefflon + + +
+
+
+
+ +
+
{children}
+
+ + ) diff --git a/src/app/login/form.tsx b/src/app/login/form.tsx index 011a12d..0c150db 100644 --- a/src/app/login/form.tsx +++ b/src/app/login/form.tsx @@ -12,6 +12,7 @@ export default function Form() { return (
{ const { email, password } = shape(fd) @@ -26,15 +27,25 @@ export default function Form() { } }} > - - - - - - - {error &&

{error}

} - - +
+ + +
+ +
+ + +
+ + {error && ( +
+ {error} +
+ )} + +
+ +
) } diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 6ef2457..f25335d 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,13 +1,33 @@ import { Metadata } from "next" +import Link from "next/link" import Form from "./form" export const metadata: Metadata = { title: "Log In" } export default function LogIn() { return ( - <> -

Log In

-
- +
+
+

+ Log In +

+

+ Access your account to manage your requests +

+
+ +
+ + +
+

+ Don't have an account?{" "} + + Sign up + +

+
+
+
) } diff --git a/src/app/page.tsx b/src/app/page.tsx index f5db09f..604ac21 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,7 +1,77 @@ +import Link from "next/link" + export default function Home() { return ( - <> -

Home

- +
+
+

+ Welcome to Tefflon +

+

+ Your government request management system +

+
+ +
+
+

+ Submit a Request +

+

+ Create and submit new requests for approval through our streamlined process. +

+ + New Request + +
+ +
+

+ View Your Requests +

+

+ Track the status and details of all your submitted requests. +

+ + View Requests + +
+
+ +
+

+ Getting Started +

+
+
+ + 1 + +
+

Create an Account

+

Sign up for a new account to get started.

+
+
+
+ + 2 + +
+

Submit Your Request

+

Fill out the request form with all required details.

+
+
+
+ + 3 + +
+

Track Progress

+

Monitor your request status and receive updates.

+
+
+
+
+
) } diff --git a/src/app/promote/form.tsx b/src/app/promote/form.tsx index b114fe4..8e602d7 100644 --- a/src/app/promote/form.tsx +++ b/src/app/promote/form.tsx @@ -8,19 +8,37 @@ export default function Form() { const [state, action] = useActionState(promoteUser, undefined) return ( - - - + +
+ + +

+ Enter the email address of the user you want to promote to admin +

+
- {state &&

{state.error}

} + {state?.error && ( +
+ {state.error} +
+ )} - + {!state?.error && state && ( +
+ User has been successfully promoted to admin. +
+ )} + +
+ +
) } diff --git a/src/app/promote/page.tsx b/src/app/promote/page.tsx index 67623e0..9b17b64 100644 --- a/src/app/promote/page.tsx +++ b/src/app/promote/page.tsx @@ -2,15 +2,34 @@ import { identify } from "@/utils/server" import { Metadata } from "next" import Form from "./form" -export const metadata: Metadata = { title: "Promote" } +export const metadata: Metadata = { title: "Promote User" } export default async function Promote() { await identify(true) return ( - <> -

Promote

-
- +
+
+

+ Promote User +

+

+ Grant admin privileges to a user account +

+
+ +
+
+

+ Admin Access Warning +

+

+ Promoting a user will grant them full admin privileges, including the ability to review and approve requests. Only promote trusted users. +

+
+ + +
+
) } diff --git a/src/app/requests/[id]/edit/actions.ts b/src/app/requests/[id]/edit/actions.ts new file mode 100644 index 0000000..f52883e --- /dev/null +++ b/src/app/requests/[id]/edit/actions.ts @@ -0,0 +1,66 @@ +"use server" + +import { db } from "@/lib/firebase" +import { shape } from "@/utils/client" +import { identify } from "@/utils/server" +import { doc, getDoc, updateDoc } from "firebase/firestore" +import { redirect, notFound } from "next/navigation" + +export const updateRequest = async (state: unknown, fd: FormData) => { + try { + // Verify user authentication + const user = await identify() + + // Extract form data + const { requestId, name, phone, destination, start, end } = shape(fd) as { + requestId: string + name: string + phone: string + destination: string + start: string + end: string + } + + if (!requestId) { + return { error: "Request ID is required", fd } + } + + // Get the existing request to verify ownership + const requestRef = doc(db, "requests", requestId) + const requestSnap = await getDoc(requestRef) + + if (!requestSnap.exists()) { + notFound() + } + + const requestData = requestSnap.data() + + // Verify user owns this request + if (requestData.uid !== user.uid) { + return { error: "Unauthorized to edit this request", fd } + } + + // Validate required fields + if (!name?.trim() || !phone?.trim() || !destination?.trim() || !start || !end) { + return { error: "All fields are required", fd } + } + + // Update the request with new data and modification timestamp + await updateDoc(requestRef, { + name: name.trim(), + phone: phone.trim(), + destination: destination.trim(), + start, + end, + modified: Date.now(), + version: (requestData.version || 1) + 1 // Increment version + }) + + // Redirect back to request details + redirect(`/requests/${requestId}`) + + } catch (error) { + console.error("Error updating request:", error) + return { error: "Failed to update request", fd } + } +} \ No newline at end of file diff --git a/src/app/requests/[id]/edit/edit-form.tsx b/src/app/requests/[id]/edit/edit-form.tsx new file mode 100644 index 0000000..3b3346d --- /dev/null +++ b/src/app/requests/[id]/edit/edit-form.tsx @@ -0,0 +1,108 @@ +"use client" + +import Pending from "@/components/pending" +import Link from "next/link" +import { useActionState } from "react" +import { updateRequest } from "./actions" + +interface EditFormProps { + requestId: string + initialData: { + name: string + phone: string + destination: string + start: string + end: string + } +} + +export default function EditForm({ requestId, initialData }: EditFormProps) { + const [state, action] = useActionState(updateRequest, undefined) + + return ( + + + +
+
+ + +
+ +
+ + +
+
+ +
+ + +

+ Please provide the specific location or address +

+
+ +
+
+ + +
+ +
+ + +
+
+ + {state?.error && ( +
+ {state.error} +
+ )} + +
+

+ Note: Saving changes will reset this request to "pending" status for admin review. +

+
+ +
+ + Cancel + + +
+
+ ) +} \ No newline at end of file diff --git a/src/app/requests/[id]/edit/page.tsx b/src/app/requests/[id]/edit/page.tsx new file mode 100644 index 0000000..58348b9 --- /dev/null +++ b/src/app/requests/[id]/edit/page.tsx @@ -0,0 +1,141 @@ +import { db } from "@/lib/firebase" +import { identify } from "@/utils/server" +import { getLatestReview, getReviewHistory } from "@/utils/reviews" +import { doc, getDoc } from "firebase/firestore" +import { Metadata } from "next" +import Link from "next/link" +import { notFound } from "next/navigation" +import EditForm from "./edit-form" + +type Props = { params: Promise<{ id: string }> } + +const getRequest = async (params: Props["params"]) => { + const { id } = await params + const request = await getDoc(doc(db, "requests", id)) + + if (!request.exists()) notFound() + return request +} + +export async function generateMetadata({ params }: Props): Promise { + const request = await getRequest(params) + const { destination } = request.data() + + return { title: `Edit: ${destination}` } +} + +export default async function EditRequest({ params }: Props) { + const user = await identify() + const request = await getRequest(params) + const requestData = request.data() + + // Verify user owns this request + if (requestData.uid !== user.uid) notFound() + + // Get review information to check if editing is allowed + const latestReview = await getLatestReview(request.id) + const reviewHistory = await getReviewHistory(request.id) + const status = latestReview?.status || "pending" + + // Only allow editing for pending or denied requests + if (status === "approved") { + return ( +
+
+

+ Cannot Edit Approved Request +

+

+ This request has been approved and cannot be edited. Contact an administrator if changes are needed. +

+ + Back to Request + +
+
+ ) + } + + const { name, phone, destination, start, end, created, modified } = requestData + const submitDate = new Date(created).toLocaleDateString() + const lastModified = modified ? new Date(modified).toLocaleDateString() : null + + // Extract form data for EditForm component + const formData = { name, phone, destination, start, end } + + return ( +
+
+
+ + Requests + + / + + {destination} + + / + Edit +
+
+

+ Edit Request +

+ + {status.charAt(0).toUpperCase() + status.slice(1)} + +
+
+

Originally submitted on {submitDate}

+ {lastModified &&

Last modified on {lastModified}

} +
+
+ + {/* Show review history as context */} + {reviewHistory.length > 0 && ( +
+
+

+ Previous Review Feedback +

+

+ Please review the feedback below and make necessary changes to your request. +

+
+
+ {reviewHistory.map((review) => ( +
+
+ + {review.status.charAt(0).toUpperCase() + review.status.slice(1)} + +

+ {new Date(review.created).toLocaleDateString()} +

+
+ {review.message && ( +
+

{review.message}

+
+ )} +
+ ))} +
+
+ )} + + {/* Edit form */} +
+
+

+ Update Request Details +

+

+ Make your changes below. This will reset the request status to pending for review. +

+
+ +
+
+ ) +} \ No newline at end of file diff --git a/src/app/requests/[id]/page.tsx b/src/app/requests/[id]/page.tsx index d5c8068..53cdf49 100644 --- a/src/app/requests/[id]/page.tsx +++ b/src/app/requests/[id]/page.tsx @@ -1,7 +1,9 @@ import { db } from "@/lib/firebase" import { identify } from "@/utils/server" +import { getLatestReview, getReviewHistory } from "@/utils/reviews" import { doc, getDoc } from "firebase/firestore" import { Metadata } from "next" +import Link from "next/link" import { notFound } from "next/navigation" type Props = { params: Promise<{ id: string }> } @@ -25,17 +27,223 @@ export default async function Request({ params }: Props) { const user = await identify() const request = await getRequest(params) - const { uid, name, destination, status } = request.data() + const { uid, name, phone, destination, start, end, created } = request.data() if (user.uid !== uid) notFound() + // Get review information + const latestReview = await getLatestReview(request.id) + const reviewHistory = await getReviewHistory(request.id) + const status = latestReview?.status || "pending" + + const submitDate = new Date(created).toLocaleDateString() + return ( - <> -

Request

+
+
+
+ + Requests + + / + Request Details +
+
+

+ Request Details +

+ + {status.charAt(0).toUpperCase() + status.slice(1)} + +
+
+ +
+
+

+ {destination} +

+

+ Submitted on {submitDate} +

+
+ +
+
+
+

+ Full Name +

+

{name}

+
+ +
+

+ Phone Number +

+

{phone}

+
+
+ +
+

+ Destination +

+

{destination}

+
+ +
+
+

+ Start Date +

+

{start}

+
+ +
+

+ End Date +

+

{end}

+
+
+
+
+ + {/* Show review status and admin message if reviewed */} + {latestReview ? ( +
+
+

+ Review Status +

+
+
+
+ + {latestReview.status.charAt(0).toUpperCase() + latestReview.status.slice(1)} + +

+ Reviewed on {new Date(latestReview.created).toLocaleDateString()} +

+
+ + {latestReview.message && ( +
+

+ {latestReview.status === "approved" ? "Approval Notes" : "Reason for Denial"} +

+
+

+ {latestReview.message} +

+
+
+ )} + + {latestReview.status === "denied" && ( +
+

+ Request Denied - Action Required +

+

+ Please review the feedback above and edit your request to address the concerns mentioned. +

+
+ + Edit This Request + + + Submit New Request + +
+
+ )} +
+
+ ) : ( +
+

+ Pending Review +

+

+ Your request is currently pending review. You will receive notification once a decision has been made. +

+
+ )} + + {/* Show review history if there are multiple reviews */} + {reviewHistory.length > 1 && ( +
+
+

+ Previous Reviews ({reviewHistory.length - 1} review{reviewHistory.length > 2 ? 's' : ''}) +

+

+ History of previous feedback on this request +

+
+
+ {reviewHistory.slice(1).map((review) => ( +
+
+ + {review.status.charAt(0).toUpperCase() + review.status.slice(1)} + +

+ {new Date(review.created).toLocaleDateString()} +

+
+ + {review.message && ( +
+

+ {review.status === "approved" ? "Approval Notes" : "Reason for Denial"} +

+
+

{review.message}

+
+
+ )} + + {!review.message && ( +

+ No message provided with this review. +

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

{name}

-

{destination}

-

{status}

- +
+ + Back to Requests + + + {/* Show edit button for pending or denied requests */} + {(status === "pending" || status === "denied") && ( + + Edit Request + + )} +
+
) } diff --git a/src/app/requests/new/actions.ts b/src/app/requests/new/actions.ts index f344508..d97318c 100644 --- a/src/app/requests/new/actions.ts +++ b/src/app/requests/new/actions.ts @@ -18,8 +18,7 @@ export const createRequest = async (state: unknown, fd: FormData) => { destination: destination.trim(), start, end, - timestamp: Date.now(), - status: "pending", + created: Date.now(), }) redirect("/requests") diff --git a/src/app/requests/new/form.tsx b/src/app/requests/new/form.tsx index d9294f5..fb8343c 100644 --- a/src/app/requests/new/form.tsx +++ b/src/app/requests/new/form.tsx @@ -1,6 +1,7 @@ "use client" import Pending from "@/components/pending" +import Link from "next/link" import { useActionState } from "react" import { createRequest } from "./actions" @@ -8,23 +9,45 @@ export default function Form() { const [state, action] = useActionState(createRequest, undefined) return ( -
- - - - - - - - - - - - - - - - + +
+
+ + +
+ +
+ + +
+
+ +
+ + +

+ Please provide the specific location or address +

+
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + Cancel + + +
) } diff --git a/src/app/requests/new/page.tsx b/src/app/requests/new/page.tsx index 520eef6..b6826c6 100644 --- a/src/app/requests/new/page.tsx +++ b/src/app/requests/new/page.tsx @@ -1,16 +1,34 @@ import { identify } from "@/utils/server" import { Metadata } from "next" +import Link from "next/link" import Form from "./form" -export const metadata: Metadata = { title: "New" } +export const metadata: Metadata = { title: "New Request" } export default async function New() { await identify() return ( - <> -

New

-
- +
+
+
+ + Requests + + / + New Request +
+

+ Submit New Request +

+

+ Fill out the form below to submit a new request for review. +

+
+ +
+ +
+
) } diff --git a/src/app/requests/page.tsx b/src/app/requests/page.tsx index e545e17..05d1258 100644 --- a/src/app/requests/page.tsx +++ b/src/app/requests/page.tsx @@ -1,31 +1,69 @@ import { db } from "@/lib/firebase" import { identify } from "@/utils/server" +import { getRequestsWithStatus } from "@/utils/reviews" import { collection, getDocs, query, where } from "firebase/firestore" import { Metadata } from "next" import Link from "next/link" +import RequestItem from "./request-item" export const metadata: Metadata = { title: "Requests" } export default async function Requests() { const { uid } = await identify() + // Get user's requests const { docs } = await getDocs( query(collection(db, "requests"), where("uid", "==", uid)), ) + // Get status for all requests from reviews collection + const requestIds = docs.map((doc) => doc.id) + const statusMap = await getRequestsWithStatus(requestIds) + return ( - <> -

Requests

+
+
+

Your Requests

+ + New Request + +
- {docs.map((request) => { - const { destination } = request.data() + {docs.length === 0 ? ( +
+
+

+ No requests yet +

+

+ You haven't submitted any requests. Get started by creating your + first request. +

+ + Create Your First Request + +
+
+ ) : ( +
+ {docs.map((request) => { + const { destination, created } = request.data() + const status = statusMap.get(request.id) || "pending" - return ( - -

{destination}

- - ) - })} - + return ( + + ) + })} +
+ )} +
) } diff --git a/src/app/requests/request-item.tsx b/src/app/requests/request-item.tsx new file mode 100644 index 0000000..0f52b62 --- /dev/null +++ b/src/app/requests/request-item.tsx @@ -0,0 +1,77 @@ +"use client" + +import Link from "next/link" +import { useRouter } from "next/navigation" + +interface RequestItemProps { + request: { + id: string + destination: string + created: number + } + status: "pending" | "approved" | "denied" +} + +export default function RequestItem({ request, status }: RequestItemProps) { + const router = useRouter() + const date = new Date(request.created).toLocaleDateString() + + return ( + +
+
+
+

+ {request.destination} +

+

+ Submitted on {date} +

+
+
+
+ + {status.charAt(0).toUpperCase() + status.slice(1)} + + {status === "denied" && ( + + )} +
+ + + +
+
+
+ + ) +} \ No newline at end of file diff --git a/src/app/review/[id]/page.tsx b/src/app/review/[id]/page.tsx index 46b1588..0f6a7ba 100644 --- a/src/app/review/[id]/page.tsx +++ b/src/app/review/[id]/page.tsx @@ -1,9 +1,11 @@ -import Pending from "@/components/pending" import { db } from "@/lib/firebase" import { identify } from "@/utils/server" -import { doc, getDoc, updateDoc } from "firebase/firestore" +import { getLatestReview, getReviewHistory, getRequestEditInfo } from "@/utils/reviews" +import { doc, getDoc } from "firebase/firestore" import { Metadata } from "next" -import { notFound, redirect } from "next/navigation" +import Link from "next/link" +import { notFound } from "next/navigation" +import ReviewForm from "./review-form" type Props = { params: Promise<{ id: string }> } @@ -19,48 +21,242 @@ export async function generateMetadata({ params }: Props): Promise { const request = await getRequest(params) const { destination } = request.data() - return { title: destination } + return { title: `Review: ${destination}` } } export default async function Request({ params }: Props) { await identify(true) const request = await getRequest(params) - const { name, destination, status } = request.data() + const requestData = request.data() + const { name, phone, destination, start, end } = requestData + + // Get review information + const latestReview = await getLatestReview(request.id) + const reviewHistory = await getReviewHistory(request.id) + const status = latestReview?.status || "pending" + + // Get edit information + const editInfo = getRequestEditInfo(requestData) + const submitDate = editInfo.created.toLocaleDateString() + + // Determine if this request needs review + const needsReview = !latestReview || + (latestReview.status === "denied" && + editInfo.isModified && + editInfo.modified && + editInfo.modified.getTime() > latestReview.created) return ( - <> -

Request

- -

{name}

-

{destination}

-

{status}

- - { - "use server" - - const ref = doc(db, "requests", request.id) - await updateDoc(ref, { status: "approved" }) - - redirect("/review") - }} - > - - - -
{ - "use server" - - const ref = doc(db, "requests", request.id) - await updateDoc(ref, { status: "denied" }) - - redirect("/review") - }} - > - - - +
+
+
+ + Review + + / + Request Review +
+
+

+ Review Request +

+ + {status.charAt(0).toUpperCase() + status.slice(1)} + +
+
+ +
+
+
+
+

+ {destination} +

+
+

Originally submitted on {submitDate}

+ {editInfo.isModified && ( +

+ Last edited on {editInfo.lastModifiedDate} (Version {editInfo.version}) +

+ )} +
+
+ {editInfo.isModified && ( + + Edited + + )} +
+
+ +
+
+
+

+ Full Name +

+

{name}

+
+ +
+

+ Phone Number +

+

{phone}

+
+
+ +
+

+ Destination +

+

{destination}

+
+ +
+
+

+ Start Date +

+

{start}

+
+ +
+

+ End Date +

+

{end}

+
+
+
+
+ + {/* Show existing review if one exists */} + {latestReview && ( +
+
+

+ Review Decision +

+
+
+
+ + {latestReview.status.charAt(0).toUpperCase() + latestReview.status.slice(1)} + +
+

Reviewed by: {latestReview.email}

+

Date: {new Date(latestReview.created).toLocaleDateString()}

+
+
+ {latestReview.message && ( +
+

+ Admin Message +

+
+

{latestReview.message}

+
+
+ )} +
+
+ )} + + {/* Review form for requests that need review */} + {needsReview && ( +
+ {latestReview?.status === "denied" && editInfo.isModified && ( +
+
+

+ Re-Review Required +

+

+ This request was previously denied but has been modified by the user since then. + Please review the updated information and make a new decision. +

+
+
+ )} + +
+ )} + +
+ + Back to Review + + {!needsReview && latestReview && ( +
+

+ This request has already been {status}. + {status === "denied" && !editInfo.isModified && ( + + The user can edit this request to submit it for re-review. + + )} +

+
+ )} + + {/* Review History for Admins */} + {reviewHistory.length > 0 && ( +
+
+

+ Review History ({reviewHistory.length} review{reviewHistory.length > 1 ? 's' : ''}) +

+
+
+ {reviewHistory.map((review, index) => ( +
+
+
+ + {review.status.charAt(0).toUpperCase() + review.status.slice(1)} + + {index === 0 && ( + + Latest + + )} +
+
+

{new Date(review.created).toLocaleDateString()} at {new Date(review.created).toLocaleTimeString()}

+

by {review.email}

+
+
+ + {review.message && ( +
+

+ Admin Message: +

+

+ {review.message} +

+
+ )} + + {!review.message && ( +

+ No message provided with this review. +

+ )} +
+ ))} +
+
+ )} +
+
) } diff --git a/src/app/review/[id]/review-form.tsx b/src/app/review/[id]/review-form.tsx new file mode 100644 index 0000000..b64f7f1 --- /dev/null +++ b/src/app/review/[id]/review-form.tsx @@ -0,0 +1,137 @@ +"use client" + +import Pending from "@/components/pending" +import { useActionState, useState } from "react" +import { approveRequest, denyRequest } from "../actions" + +interface ReviewFormProps { + requestId: string +} + +export default function ReviewForm({ requestId }: ReviewFormProps) { + const [approveState, approveAction] = useActionState(approveRequest, undefined) + const [denyState, denyAction] = useActionState(denyRequest, undefined) + const [approveMessage, setApproveMessage] = useState("") + const [denyMessage, setDenyMessage] = useState("") + const [activeForm, setActiveForm] = useState<"approve" | "deny" | null>(null) + + const maxMessageLength = 500 + + return ( +
+

+ Review Actions +

+

+ Please review the request details above and choose an action below. + You can optionally add a message to explain your decision. +

+ + {/* Quick action buttons when no form is active */} + {!activeForm && ( +
+ + +
+ )} + + {/* Approve form */} + {activeForm === "approve" && ( +
+ + +
+ +