diff --git a/.env.example b/.env.example index 74588c0..5871316 100644 --- a/.env.example +++ b/.env.example @@ -10,48 +10,69 @@ # When adding additional environment variables, the schema in "/src/env.js" # should be updated accordingly. -# App URL -NEXT_PUBLIC_APP_URL="http://localhost:3000" - -# Node environment +# ────────────────────────────────────────────── +# Core +# ────────────────────────────────────────────── NODE_ENV="development" +NEXT_PUBLIC_APP_URL="http://localhost:3000" -# Database +# ────────────────────────────────────────────── +# Database (PostgreSQL) +# ────────────────────────────────────────────── DATABASE_URL="postgresql://postgres:password@localhost:5432/trackit-saas" -# Optional: Prisma Accelerate service URL (leave empty if not using Prisma Accelerate) -PRISMA_ACCELERATE_URL="" -# Better Auth +# ────────────────────────────────────────────── +# Authentication (Better Auth) +# ────────────────────────────────────────────── BETTER_AUTH_SECRET="your_secret_here" BETTER_AUTH_URL="http://localhost:3000" -# Inngest (async jobs & schedules) +# ────────────────────────────────────────────── +# Email - Primary: Resend (optional, needs paid plan) +# ────────────────────────────────────────────── +RESEND_API_KEY="your_resend_api_key" +EMAIL_FROM="noreply@yourdomain.com" + +# ────────────────────────────────────────────── +# Email - Fallback: SMTP (e.g. Gmail, Mailtrap) +# ────────────────────────────────────────────── +SMTP_HOST="smtp.gmail.com" +SMTP_PORT=587 +SMTP_USER="you@gmail.com" +SMTP_PASS="your_app_password" +SMTP_FROM="Trackit " + +# ────────────────────────────────────────────── +# AI (Google Gemini) — server-side only +# ────────────────────────────────────────────── +GEMINI_API_KEY="your_gemini_api_key" +GEMINI_MODEL="gemini-2.5-flash" +GEMINI_MAX_ROWS=50 +# Client-side row limit (for bulk import UI validation) +NEXT_PUBLIC_GEMINI_MAX_ROWS=50 + +# ────────────────────────────────────────────── +# Background Jobs (Inngest) — optional +# ────────────────────────────────────────────── INNGEST_EVENT_KEY="your_inngest_event_key" INNGEST_SIGNING_KEY="your_inngest_signing_key" -# Better Stack Logging -NEXT_PUBLIC_BETTER_STACK_SOURCE_TOKEN= -NEXT_PUBLIC_BETTER_STACK_INGESTING_URL= -NEXT_PUBLIC_BETTER_STACK_LOG_LEVEL=debug - -# ImageKit.io for image uploads +# ────────────────────────────────────────────── +# Image Uploads (ImageKit) — optional +# ────────────────────────────────────────────── IMAGEKIT_PRIVATE_KEY="your_imagekit_private_key" -IMAGEKIT_URL_ENDPOINT="https://ik.imagekit.io/your_imagekit_id" NEXT_PUBLIC_IMAGEKIT_PUBLIC_KEY="your_imagekit_public_key" NEXT_PUBLIC_IMAGEKIT_URL_ENDPOINT="https://ik.imagekit.io/your_imagekit_id" -# RESEND API for transactional emails -RESEND_API_KEY="your_resend_api_key" -EMAIL_FROM="your@email.com" - -# Gemini AI Service -GEMINI_MAX_ROWS=50 -GEMINI_API_KEY='your_gemini_api_key' -GEMINI_MODEL='gemini-2.5-flash' -NEXT_PUBLIC_GEMINI_API_KEY='your_gemini_api_key' -NEXT_PUBLIC_GEMINI_MODEL='gemini-2.5-flash' -NEXT_PUBLIC_GEMINI_MAX_ROWS=50 +# ────────────────────────────────────────────── +# Logging (Better Stack / Logtail) +# ────────────────────────────────────────────── +NEXT_PUBLIC_BETTER_STACK_SOURCE_TOKEN="your_source_token" +NEXT_PUBLIC_BETTER_STACK_INGESTING_URL="https://in.logs.betterstack.com" +NEXT_PUBLIC_BETTER_STACK_LOG_LEVEL="debug" -# Upstash Redis for rate limiting and caching +# ────────────────────────────────────────────── +# Rate Limiting (Upstash Redis) — optional +# ────────────────────────────────────────────── UPSTASH_REDIS_REST_URL="your_upstash_redis_rest_url" -UPSTASH_REDIS_REST_TOKEN="your_upstash_redis_rest_token" \ No newline at end of file +UPSTASH_REDIS_REST_TOKEN="your_upstash_redis_rest_token" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a69df0..5ed776b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v2 with: - version: 10 + version: 10.15.0 - name: Cache pnpm store uses: actions/cache@v4 @@ -58,5 +58,10 @@ jobs: - name: Type check run: pnpm run typecheck - - name: Run Format check + - name: Run Format check run: pnpm run format:check + + - name: Build + env: + SKIP_ENV_VALIDATION: 'true' + run: pnpm run build diff --git a/.gitignore b/.gitignore index 4ac3bd8..b578207 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,5 @@ yarn-error.log* test-results/ playwright-report/ CLAUDE.md -.claude/ \ No newline at end of file +.claude/ +.planning/ \ No newline at end of file diff --git a/GAP.md b/GAP.md deleted file mode 100644 index 569bb7b..0000000 --- a/GAP.md +++ /dev/null @@ -1,452 +0,0 @@ -# GAP.md - Trackit SaaS Optimization Roadmap - -> Comprehensive gap analysis across server, frontend, hooks, stores, and architecture. -> Organized into incremental phases — each phase is independently shippable. - ---- - -## Phase 1: Quick Wins (Low Effort, High Impact) - -### 1.1 Reduce Transaction Batch Sizes - -**Files:** `overview/page.tsx`, `analytics/page.tsx`, `budget/page.tsx` - -- All protected pages load `limit: 500` transactions when only displaying 10-20 items -- Change to `limit: 100-150` for overview/analytics; use proper server-side pagination for transaction list -- **Impact:** 3-5x bandwidth reduction, faster initial load - -### 1.2 Move Constants Outside Components - -**Files:** Multiple pages and components - -| File | Constant | Issue | -| ------------------------ | -------------------------------------------------------- | -------------------------------------------------------------------------------- | -| `accounts/page.tsx` | `ICONS.find()` in AccountCard | Linear search O(n) per render → pre-compute `ICON_MAP` as `Record` | -| `account-form.tsx` | `ACCOUNT_TYPES`, `CURRENCIES` | Recreated every render → move to module level | -| `transactions-table.tsx` | `methodMap` (line ~406) | Object in cell renderer → move to constant | -| `reports/page.tsx` | `getStatusBadge`, `getTypeLabel`, `getReportDescription` | Functions inside component → move outside | -| `bar-chart.tsx` | `formatAxisValue` | Already outside (good) | - -### 1.3 Wrap Layout Components with React.memo - -**Files:** `navbar.tsx`, `dashboard-header.tsx` - -- These re-render on every route change despite no prop changes -- Wrap exports with `React.memo()` -- **Impact:** Prevents unnecessary re-renders during navigation - -### 1.4 Use `useFormatter` Hook Consistently - -**Files:** `accounts/page.tsx` (line ~270), `accounts/[id]/page.tsx` (line ~46) - -- Both create `new Intl.NumberFormat()` inline on every render -- Replace with existing `useFormatter()` hook for consistency and memoization - -### 1.5 Combine Related useState Calls - -**Files:** `accounts/page.tsx`, `accounts/[id]/page.tsx`, `profile/page.tsx` - -- Accounts page has 5+ scattered `useState` calls for modal/dialog state -- Combine into a single `uiState` object to batch updates and reduce re-renders: - ```ts - const [uiState, setUiState] = useState({ - modal: null as "create" | "edit" | "delete" | null, - selectedId: null as string | null, - showFilters: false, - layoutMode: "grid" as "grid" | "list", - }); - ``` - ---- - -## Phase 2: Decimal & Data Layer Standardization - -### 2.1 Create Unified Decimal Utility - -**New file:** `src/lib/shared/decimal.ts` - -- Found **45+ inconsistent Decimal conversions** across the codebase: - - `.toNumber()` — 15 occurrences - - `Number()` — 25 occurrences - - `toString()` — 5+ occurrences - - Defensive ternary pattern — 10+ occurrences (especially in budget page, AI service) -- Create a single `toNum(value: unknown): number` helper and use it everywhere -- **Files affected:** `transactionRouter.ts`, `accountRouter.ts`, `overviewRouter.ts`, `budgetService.ts`, `reportService.ts`, `aiService.ts`, `budget/page.tsx`, all Inngest workers -- **Impact:** Eliminates 40+ lines of defensive code, prevents precision bugs - -### 2.2 Optimize Prisma Selects - -**Files:** All routers in `src/server/api/routers/` - -- Category list returns all fields when only ID/name needed -- Transaction list fetches full `recurringRule` data for every row -- Add `select` clauses to return only needed fields: - ```ts - // categoryRouter.ts - select: { id: true, name: true, color: true, children: { select: { id: true, name: true } } } - ``` -- **Impact:** 35-50% payload reduction, 20-30% API response improvement - -### 2.3 Extract AI Service Boilerplate - -**File:** `src/server/services/aiService.ts` (~827 LOC) - -- Repeated Decimal conversion pattern (lines 77-81, 154-160, 169-173, 227-234, 327-329, 337-341) -- Repeated JSON extraction regex (lines 108-110, 196-199, 287-290, 365-367) -- Extract both into shared utilities -- **Impact:** Reduces aiService by ~100 LOC, easier maintenance - -### 2.4 Standardize Query Invalidation - -**New file:** `src/lib/trpc/invalidation.ts` - -- Create centralized invalidation map so all mutations invalidate related queries consistently: - ```ts - export const invalidate = { - transaction: (utils) => { - utils.transaction.list.invalidate(); - utils.account.list.invalidate(); - utils.overview.invalidate(); - }, - account: (utils) => { - utils.account.list.invalidate(); - utils.overview.invalidate(); - }, - budget: (utils) => { - utils.budget.all.invalidate(); - }, - }; - ``` -- **Impact:** Prevents stale data, consistent cache behavior - ---- - -## Phase 3: Component Architecture & Memoization - -### 3.1 Extract Nested Components - -**Priority extractions:** - -| Current Location | Extract To | Reason | -| ------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------- | -| `accounts/page.tsx` AccountCard (line ~266) | `src/components/pages/(protected)/accounts/account-card.tsx` | Cannot memo while nested; recreated every parent render | -| `overview/page.tsx` spending section | Separate component | Giant 147-line useMemo could be split | - -- Wrap extracted components with `React.memo` -- **Impact:** Eliminates re-renders of unchanged cards when search/layout changes - -### 3.2 Split Overview Page Mega-Memo - -**File:** `overview/page.tsx` (lines 106-253) - -- Single 147-line `useMemo` computes stats + bar chart + pie chart simultaneously -- Split into 3 isolated `useMemo` hooks with independent dependency arrays -- When `barRange` changes, only bar chart data recalculates (not stats or pie chart) -- **Impact:** Granular memoization, prevents unnecessary recalculations - -### 3.3 Add React.memo to Presentational Components - -Components that should be wrapped: - -- `StatsCards` — props change infrequently -- `BudgetCard` — pure presentational -- `TransactionTypeBadge` — pure presentational -- Chart components (`BarChart`, `PieChart`, `AreaChart`) -- `AccountCard` (after extraction) - -### 3.4 Add useCallback to Event Handlers - -**Files:** `transactions/page.tsx`, `accounts/page.tsx`, `budget/page.tsx` - -- `handleDeleteTransactions`, `handleEditTransaction`, modal open/close handlers — all recreated every render -- Wrap with `useCallback` so memoized children don't re-render -- **Impact:** 40-60% reduction in unnecessary child re-renders - -### 3.5 Split Transaction Form (1,331 LOC) - -**File:** `src/components/forms/transaction/transaction-form.tsx` - -- Massive single-file component handling category selection, recurring rules, receipt upload, all validation -- Split into sub-components: - ``` - forms/transaction/ - transaction-form.tsx (~300 lines) - wrapper + orchestration - steps/basic-info-step.tsx (~200 lines) - amount, date, description - steps/category-step.tsx (~300 lines) - category selector - steps/recurring-step.tsx (~250 lines) - recurrence config - steps/receipt-step.tsx (~200 lines) - receipt upload - ``` -- Extract file upload logic to `useReceiptUpload` custom hook -- Combine `form.watch("isRecurring")` + `form.watch("recurrence")` into single subscription -- **Impact:** Smaller bundles per step, easier testing, allows lazy loading - ---- - -## Phase 4: Performance — Virtualization, Code Splitting, Suspense - -### 4.1 Add Virtual Scrolling to TransactionsTable - -**File:** `src/components/common/transactions-table.tsx` - -**Package:** `@tanstack/react-virtual` - -- Table renders all filtered rows in the DOM even with pagination -- For users with 1000+ transactions, this causes significant DOM bloat -- Integrate virtualizer with existing TanStack Table: - ```ts - import { useVirtualizer } from "@tanstack/react-virtual"; - const rowVirtualizer = useVirtualizer({ - count: table.getRowModel().rows.length, - getScrollElement: () => scrollContainerRef.current, - estimateSize: () => 48, - overscan: 10, - }); - ``` -- **Impact:** DOM nodes from 500+ to ~20, 70% render improvement, 60fps scroll - -### 4.2 Lazy Load Heavy Components - -**Files:** Multiple pages - -- Transaction form, bulk import dialog, chart components, profile/settings sections — all loaded upfront -- Use `next/dynamic` for modals and heavy sections: - ```ts - const TransactionForm = dynamic( - () => import("@/components/forms/transaction/transaction-form"), - { ssr: false, loading: () => } - ); - ``` -- Apply to: TransactionForm, BulkImportDialog, profile sections, settings sections, chart components -- **Impact:** 20-30% initial JS bundle reduction, 150-300ms LCP improvement - -### 4.3 Add Suspense Boundaries - -**Files:** All protected pages - -- No Suspense components used — entire page waits for all data -- Wrap independent sections in Suspense for progressive rendering: - ```tsx - }> - - - }> - - - ``` -- **Impact:** Better perceived performance, 200-400ms improvement in time-to-interactive - -### 4.4 Add Bundle Analyzer - -**Package:** `@next/bundle-analyzer` - -- No visibility into bundle composition currently -- Add analyzer to `next.config.js`: - ```ts - import withBundleAnalyzer from "@next/bundle-analyzer"; - const withAnalyzer = withBundleAnalyzer({ - enabled: process.env.ANALYZE === "true", - }); - ``` -- Run with `ANALYZE=true pnpm build` to identify largest chunks -- **Impact:** Data-driven optimization decisions - -### 4.5 Prefetch on Navigation Intent - -- No data prefetching when user hovers navigation links -- Add prefetch on hover/focus for anticipated routes: - ```ts - onMouseEnter={() => utils.transaction.list.prefetch({ limit: 50 })} - ``` -- **Impact:** Faster perceived navigation - ---- - -## Phase 5: Backend & Service Layer Optimization - -### 5.1 Move In-Memory Aggregations to Database - -**Files:** `overviewRouter.ts`, `reportService.ts` - -- `balanceOverview` (overviewRouter line ~157) loads ALL transactions for 24 months into memory, groups in JS -- `reportService` runs 5 parallel queries that could be 1-2 combined queries -- Replace with Prisma `groupBy` or raw SQL aggregation: - ```ts - const monthly = await ctx.db.transaction.groupBy({ - by: ["date"], - _sum: { amount: true }, - where: { userId, date: { gte: startDate } }, - }); - ``` -- **Impact:** O(1) memory instead of O(n), prevents OOM with large datasets - -### 5.2 Fix N+1 Query Patterns - -| Location | Issue | Fix | -| ------------------------------------------------ | ------------------------------------------ | ------------------------------------------- | -| `categoryRouter.ts` delete (lines 133-146) | Recursive queries per child level | Single `findMany` + batch `deleteMany` | -| `budgetService.ts` evaluateBudgets (lines 19-52) | Queries budgets per category in loop | Pre-load all user budgets, filter in-memory | -| `aiService.ts` | Multiple separate queries for related data | Combine with `include` or batch | - -### 5.3 Add Transaction Wrapping for Critical Operations - -**Files:** `notificationService.ts`, `budgetService.ts` - -- `notificationService.createNotification` — notification + email queue not atomic -- `budgetService.checkThresholds` — update + event emit not atomic -- Wrap in `prisma.$transaction()` to prevent partial updates - -### 5.4 Remove Unnecessary Transaction Wrapping - -**File:** `accountRouter.ts` (lines 110-115, 175-180) - -- `$transaction` wrapping single `updateMany` call — overhead without benefit -- Replace with direct `await prisma.bankAccount.updateMany(...)` - -### 5.5 Extract Rate Limit Middleware for AI Router - -**File:** `aiRouter.ts` - -- Same rate limit check duplicated 6 times (lines 13-18, 26-31, 37-42, 47-52, 61-66, 78-83) -- Create reusable middleware: - ```ts - const aiRateLimitedProcedure = protectedProcedure.use( - async ({ ctx, next }) => { - checkRateLimit(ctx.user.id, "ai", AI_MAX_REQUESTS); - return next(); - }, - ); - ``` - -### 5.6 Fix Error Handling Inconsistencies - -| File | Issue | -| ---------------------------- | -------------------------------------------------------- | -| `userRouter.ts` line 187 | Throws `Error` instead of `TRPCError` | -| `aiService.ts` lines 396-410 | Returns empty `{ results: [] }` instead of throwing | -| `reportService.ts` line 125 | Throws generic `Error` instead of `TRPCError` | -| `accountRouter.ts` | No cascade delete validation — could orphan transactions | - -### 5.7 Fix Timing Middleware Production Leak - -**File:** `src/server/api/trpc.ts` (lines 217-225) - -- `console.log` / `timingLogger.info()` runs in all environments including production -- Random 100-500ms delay blocks every tRPC call in development -- Guard with `process.env.NODE_ENV !== 'production'` - ---- - -## Phase 6: Inngest Workers & Email Templates - -### 6.1 Fix File System I/O in Workers - -**Files:** `budget.ts`, `send-budget-alert-email.ts`, `generate-monthly-report.ts` - -- All read templates from disk via `process.cwd()` — fails in Docker/serverless -- Options: - - Inline templates as string constants - - Use environment variables for template paths - - Pre-compile templates at build time -- **Impact:** Prevents deployment failures - -### 6.2 Deduplicate Decimal Handling in Workers - -- Same defensive Decimal conversion in every worker -- After Phase 2.1's `toNum()` utility, import it in all workers - -### 6.3 Add Retry Mechanism for AI Rate Limits - -**File:** `aiService.ts` - -- Retries on parse errors but not API rate limits -- Add exponential backoff for `429 Too Many Requests` - ---- - -## Phase 7: Advanced — RSC Migration & Streaming - -### 7.1 Convert Pages to Server Components Where Possible - -**Files:** All 19 protected pages currently have `"use client"` - -- Many pages could be Server Components that fetch data directly and pass to client children -- Pattern: - ```tsx - // page.tsx (Server Component — no "use client") - import { api, HydrateClient } from "@/trpc/server"; - export default async function OverviewPage() { - void api.transaction.list.prefetch({ limit: 100 }); - return ( - - - - ); - } - ``` -- Start with simpler pages (reports, settings, profile) then move to complex ones -- **Impact:** 15-25% JS payload reduction, faster FCP - -### 7.2 Add Streaming with Parallel Data Loading - -- Use React Server Components + Suspense for streaming -- Independent sections load and render as data arrives -- **Impact:** Better perceived performance, especially on slower connections - -### 7.3 Redis-Backed Rate Limiting - -**File:** `src/server/api/rateLimit.ts` - -- Current in-memory rate limiting fails in distributed deployments and across server restarts -- Migrate to Redis (via Upstash or ioredis) for production -- Add `RateLimit-Remaining` and `RateLimit-Reset` response headers - -### 7.4 Additional TypeScript Strictness - -**File:** `tsconfig.json` - -Add: - -```json -{ - "noFallthroughCasesInSwitch": true, - "noImplicitReturns": true, - "noUnusedLocals": true, - "noUnusedParameters": true -} -``` - -- **Impact:** Catches 5-10% more potential bugs at compile time - ---- - -## Summary: Estimated Impact by Phase - -| Phase | Effort | Bundle Impact | Performance Impact | Risk | -| ------------------------ | ----------- | ------------- | --------------------------- | -------- | -| **Phase 1: Quick Wins** | 2-3 hours | -5% | +15% render perf | Very Low | -| **Phase 2: Data Layer** | 4-5 hours | -2% | +20-30% API speed | Low | -| **Phase 3: Components** | 6-8 hours | -5% | +40-60% re-render reduction | Low | -| **Phase 4: Performance** | 8-12 hours | -20-30% | +200-400ms LCP/TTI | Medium | -| **Phase 5: Backend** | 6-8 hours | None | +30-50% query perf | Medium | -| **Phase 6: Workers** | 3-4 hours | None | Deployment reliability | Low | -| **Phase 7: Advanced** | 10-15 hours | -15-25% JS | +200-400ms FCP | Higher | - ---- - -## Key Packages to Add - -| Package | Purpose | Phase | -| ----------------------------- | ---------------------------------- | ------- | -| `@tanstack/react-virtual` | Virtual scrolling for tables/lists | Phase 4 | -| `@next/bundle-analyzer` | Bundle size visibility | Phase 4 | -| `superjson` (already present) | Decimal serialization (use more) | Phase 2 | - -## Key Packages Already In Use (Leverage Better) - -| Package | Current Usage | Better Usage | -| ---------------------------------- | -------------------- | ------------------------------------------------------- | -| `react-hook-form` | Used in forms | Consolidate all inline validation to Zod schemas | -| `@tanstack/react-query` (via tRPC) | Default staleTime | Per-endpoint staleTime, prefetching, optimistic updates | -| `@tanstack/react-table` | Used in transactions | Add virtualization layer on top | -| `zustand` | Single store | Could add slices for complex pages | -| `next/dynamic` | 4 uses | Apply to all heavy modals/forms/charts | diff --git a/README.md b/README.md index 1534996..16dc995 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ A personal finance SaaS for bank account management, transaction tracking, hierarchical budgeting, AI-powered insights, and automated reporting. +

Next.js React TypeScript @@ -10,7 +11,8 @@ A personal finance SaaS for bank account management, transaction tracking, hiera PostgreSQL Tailwind CSS shadcn/ui - +

+

Google Gemini Inngest Resend @@ -18,6 +20,7 @@ A personal finance SaaS for bank account management, transaction tracking, hiera Better Auth Better Stack ImageKit +

--- diff --git a/middleware.ts b/middleware.ts index a82a285..169fbac 100644 --- a/middleware.ts +++ b/middleware.ts @@ -43,6 +43,16 @@ function isAuthPath(pathname: string) { return AUTH_PATHS.some((p) => pathname === p || pathname.startsWith(p + "/")); } +/** Guard against open redirect attacks — only allow relative paths. */ +function isValidRedirectPath(path: string): boolean { + return ( + path.startsWith("/") && + !path.startsWith("//") && + !path.includes(":\\") && + !/^\/[^/]*@/.test(path) + ); +} + /** * Heuristic check for an authenticated request. * @@ -100,7 +110,9 @@ export function middleware(req: NextRequest) { if (!isAuth) { const url = req.nextUrl.clone(); url.pathname = "/sign-in"; - url.searchParams.set("redirectTo", pathname); + if (isValidRedirectPath(pathname)) { + url.searchParams.set("redirectTo", pathname); + } return NextResponse.redirect(url); } diff --git a/next.config.js b/next.config.js index 6fd9105..a383cfc 100644 --- a/next.config.js +++ b/next.config.js @@ -15,6 +15,10 @@ const withBundleAnalyzer = bundleAnalyzer({ const config = {}; +config.outputFileTracingIncludes = { + "/api/**": ["./src/lib/email/templates/**"], +}; + config.images = { remotePatterns: [ { @@ -61,6 +65,11 @@ config.headers = async () => [ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()", }, + { + key: "Content-Security-Policy", + value: + "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data: blob:; font-src 'self' data:; connect-src 'self' https:; frame-ancestors 'none';", + }, ], }, ]; diff --git a/package.json b/package.json index e96c792..a179b7b 100644 --- a/package.json +++ b/package.json @@ -83,14 +83,15 @@ "input-otp": "^1.4.2", "lucide-react": "^0.545.0", "motion": "^12.23.24", - "next": "15.5.4", + "next": "15.5.9", "next-themes": "^0.4.6", + "nodemailer": "^8.0.2", "pg": "^8.16.3", "prismjs": "^1.30.0", "radix-ui": "^1.4.3", - "react": "^19.0.0", + "react": "^19.2.4", "react-day-picker": "^9.11.3", - "react-dom": "^19.0.0", + "react-dom": "^19.2.4", "react-hook-form": "^7.65.0", "react-markdown": "^10.1.0", "react-resizable-panels": "^3.0.6", @@ -105,8 +106,7 @@ "superjson": "^2.2.1", "tailwind-merge": "^3.3.1", "vaul": "^1.1.2", - "zod": "^3.24.2", - "zustand": "^5.0.8" + "zod": "^3.24.2" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", @@ -117,12 +117,13 @@ "@tailwindcss/postcss": "^4.0.15", "@tailwindcss/typography": "^0.5.19", "@types/node": "^20.14.10", + "@types/nodemailer": "^7.0.11", "@types/pg": "^8.16.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "dotenv": "^16.0.0", "eslint": "^9.23.0", - "eslint-config-next": "^15.2.3", + "eslint-config-next": "^15.5.9", "husky": "^9.1.7", "lint-staged": "^16.2.6", "postcss": "^8.5.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 503de03..22518bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,112 +13,112 @@ importers: version: 0.24.1 '@hookform/resolvers': specifier: ^5.2.2 - version: 5.2.2(react-hook-form@7.65.0(react@19.2.0)) + version: 5.2.2(react-hook-form@7.65.0(react@19.2.4)) '@logtail/next': specifier: ^0.2.1 - version: 0.2.1(next@15.5.4(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0) + version: 0.2.1(next@15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) '@prisma/adapter-pg': specifier: ^7.0.0 version: 7.0.0 '@prisma/client': specifier: ^7.0.0 - version: 7.0.0(prisma@7.0.0(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3))(typescript@5.9.3) + version: 7.0.0(prisma@7.0.0(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3) '@radix-ui/react-accordion': specifier: ^1.2.12 - version: 1.2.12(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.2.12(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-alert-dialog': specifier: ^1.1.15 - version: 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-aspect-ratio': specifier: ^1.1.7 - version: 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-avatar': specifier: ^1.1.10 - version: 1.1.10(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.1.10(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-checkbox': specifier: ^1.3.3 - version: 1.3.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.3.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-collapsible': specifier: ^1.1.12 - version: 1.1.12(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.1.12(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-context-menu': specifier: ^2.2.16 - version: 2.2.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 2.2.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-dialog': specifier: ^1.1.15 - version: 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-dropdown-menu': specifier: ^2.1.16 - version: 2.1.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 2.1.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-hover-card': specifier: ^1.1.15 - version: 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-label': specifier: ^2.1.7 - version: 2.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 2.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-menubar': specifier: ^1.1.16 - version: 1.1.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.1.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-navigation-menu': specifier: ^1.2.14 - version: 1.2.14(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.2.14(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-popover': specifier: ^1.1.15 - version: 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-progress': specifier: ^1.1.7 - version: 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-radio-group': specifier: ^1.3.8 - version: 1.3.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.3.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-scroll-area': specifier: ^1.2.10 - version: 1.2.10(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.2.10(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-select': specifier: ^2.2.6 - version: 2.2.6(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 2.2.6(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-separator': specifier: ^1.1.7 - version: 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-slider': specifier: ^1.3.6 - version: 1.3.6(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.3.6(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-slot': specifier: ^1.2.3 - version: 1.2.3(@types/react@19.2.2)(react@19.2.0) + version: 1.2.3(@types/react@19.2.2)(react@19.2.4) '@radix-ui/react-switch': specifier: ^1.2.6 - version: 1.2.6(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.2.6(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-tabs': specifier: ^1.1.13 - version: 1.1.13(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.1.13(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-toggle': specifier: ^1.1.10 - version: 1.1.10(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.1.10(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-toggle-group': specifier: ^1.1.11 - version: 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-tooltip': specifier: ^1.2.8 - version: 1.2.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.2.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@t3-oss/env-nextjs': specifier: ^0.12.0 version: 0.12.0(typescript@5.9.3)(valibot@1.1.0(typescript@5.9.3))(zod@3.25.76) '@tabler/icons-react': specifier: ^3.35.0 - version: 3.35.0(react@19.2.0) + version: 3.35.0(react@19.2.4) '@tanstack/react-query': specifier: ^5.69.0 - version: 5.90.2(react@19.2.0) + version: 5.90.2(react@19.2.4) '@tanstack/react-table': specifier: ^8.21.3 - version: 8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@trpc/client': specifier: ^11.0.0 version: 11.6.0(@trpc/server@11.6.0(typescript@5.9.3))(typescript@5.9.3) '@trpc/react-query': specifier: ^11.0.0 - version: 11.6.0(@tanstack/react-query@5.90.2(react@19.2.0))(@trpc/client@11.6.0(@trpc/server@11.6.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) + version: 11.6.0(@tanstack/react-query@5.90.2(react@19.2.4))(@trpc/client@11.6.0(@trpc/server@11.6.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.6.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) '@trpc/server': specifier: ^11.0.0 version: 11.6.0(typescript@5.9.3) @@ -127,7 +127,7 @@ importers: version: 3.9.1 '@tsparticles/react': specifier: ^3.0.0 - version: 3.0.0(@tsparticles/engine@3.9.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.0.0(@tsparticles/engine@3.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tsparticles/slim': specifier: ^3.9.1 version: 3.9.1 @@ -139,7 +139,7 @@ importers: version: 1.37.0 better-auth: specifier: ^1.3.27 - version: 1.3.27(next@15.5.4(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.3.27(next@15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -148,7 +148,7 @@ importers: version: 2.1.1 cmdk: specifier: ^1.1.1 - version: 1.1.1(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.1.1(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) cobe: specifier: ^0.6.5 version: 0.6.5 @@ -160,10 +160,10 @@ importers: version: 2.2.3 embla-carousel-react: specifier: ^8.6.0 - version: 8.6.0(react@19.2.0) + version: 8.6.0(react@19.2.4) framer-motion: specifier: ^12.23.24 - version: 12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 12.23.24(react-dom@19.2.4(react@19.2.4))(react@19.2.4) handlebars: specifier: ^4.7.8 version: 4.7.8 @@ -172,22 +172,25 @@ importers: version: 5.0.2 inngest: specifier: ^3.18.0 - version: 3.47.0(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(hono@4.7.10)(next@15.5.4(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@5.9.3)(zod@3.25.76) + version: 3.47.0(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(hono@4.7.10)(next@15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(zod@3.25.76) input-otp: specifier: ^1.4.2 - version: 1.4.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.4.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) lucide-react: specifier: ^0.545.0 - version: 0.545.0(react@19.2.0) + version: 0.545.0(react@19.2.4) motion: specifier: ^12.23.24 - version: 12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 12.23.24(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next: - specifier: 15.5.4 - version: 15.5.4(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + specifier: 15.5.9 + version: 15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-themes: specifier: ^0.4.6 - version: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + nodemailer: + specifier: ^8.0.2 + version: 8.0.2 pg: specifier: ^8.16.3 version: 8.16.3 @@ -196,31 +199,31 @@ importers: version: 1.30.0 radix-ui: specifier: ^1.4.3 - version: 1.4.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.4.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: - specifier: ^19.0.0 - version: 19.2.0 + specifier: ^19.2.4 + version: 19.2.4 react-day-picker: specifier: ^9.11.3 - version: 9.11.3(react@19.2.0) + version: 9.11.3(react@19.2.4) react-dom: - specifier: ^19.0.0 - version: 19.2.0(react@19.2.0) + specifier: ^19.2.4 + version: 19.2.4(react@19.2.4) react-hook-form: specifier: ^7.65.0 - version: 7.65.0(react@19.2.0) + version: 7.65.0(react@19.2.4) react-markdown: specifier: ^10.1.0 - version: 10.1.0(@types/react@19.2.2)(react@19.2.0) + version: 10.1.0(@types/react@19.2.2)(react@19.2.4) react-resizable-panels: specifier: ^3.0.6 - version: 3.0.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.0.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-wrap-balancer: specifier: ^1.1.1 - version: 1.1.1(react@19.2.0) + version: 1.1.1(react@19.2.4) recharts: specifier: 2.15.4 - version: 2.15.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 2.15.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) rehype-prism-plus: specifier: ^2.0.1 version: 2.0.1 @@ -238,7 +241,7 @@ importers: version: 0.0.1 sonner: specifier: ^2.0.7 - version: 2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) superjson: specifier: ^2.2.1 version: 2.2.2 @@ -247,13 +250,10 @@ importers: version: 3.3.1 vaul: specifier: ^1.1.2 - version: 1.1.2(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.1.2(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) zod: specifier: ^3.24.2 version: 3.25.76 - zustand: - specifier: ^5.0.8 - version: 5.0.8(@types/react@19.2.2)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)) devDependencies: '@eslint/eslintrc': specifier: ^3.3.1 @@ -279,6 +279,9 @@ importers: '@types/node': specifier: ^20.14.10 version: 20.19.21 + '@types/nodemailer': + specifier: ^7.0.11 + version: 7.0.11 '@types/pg': specifier: ^8.16.0 version: 8.16.0 @@ -295,8 +298,8 @@ importers: specifier: ^9.23.0 version: 9.37.0(jiti@2.6.1) eslint-config-next: - specifier: ^15.2.3 - version: 15.5.4(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + specifier: ^15.5.9 + version: 15.5.9(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) husky: specifier: ^9.1.7 version: 9.1.7 @@ -314,7 +317,7 @@ importers: version: 0.6.14(prettier@3.6.2) prisma: specifier: ^7.0.0 - version: 7.0.0(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) + version: 7.0.0(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) semantic-release: specifier: ^25.0.1 version: 25.0.1(typescript@5.9.3) @@ -864,56 +867,56 @@ packages: '@next/bundle-analyzer@16.1.6': resolution: {integrity: sha512-ee2kagdTaeEWPlotgdTOqFHYcD3e2m2bbE3I9Rq2i6ABYi5OgopmtEUe8NM23viaYxLV2tDH/2nd5+qKoEr6cw==} - '@next/env@15.5.4': - resolution: {integrity: sha512-27SQhYp5QryzIT5uO8hq99C69eLQ7qkzkDPsk3N+GuS2XgOgoYEeOav7Pf8Tn4drECOVDsDg8oj+/DVy8qQL2A==} + '@next/env@15.5.9': + resolution: {integrity: sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==} - '@next/eslint-plugin-next@15.5.4': - resolution: {integrity: sha512-SR1vhXNNg16T4zffhJ4TS7Xn7eq4NfKfcOsRwea7RIAHrjRpI9ALYbamqIJqkAhowLlERffiwk0FMvTLNdnVtw==} + '@next/eslint-plugin-next@15.5.9': + resolution: {integrity: sha512-kUzXx0iFiXw27cQAViE1yKWnz/nF8JzRmwgMRTMh8qMY90crNsdXJRh2e+R0vBpFR3kk1yvAR7wev7+fCCb79Q==} - '@next/swc-darwin-arm64@15.5.4': - resolution: {integrity: sha512-nopqz+Ov6uvorej8ndRX6HlxCYWCO3AHLfKK2TYvxoSB2scETOcfm/HSS3piPqc3A+MUgyHoqE6je4wnkjfrOA==} + '@next/swc-darwin-arm64@15.5.7': + resolution: {integrity: sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.5.4': - resolution: {integrity: sha512-QOTCFq8b09ghfjRJKfb68kU9k2K+2wsC4A67psOiMn849K9ZXgCSRQr0oVHfmKnoqCbEmQWG1f2h1T2vtJJ9mA==} + '@next/swc-darwin-x64@15.5.7': + resolution: {integrity: sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.5.4': - resolution: {integrity: sha512-eRD5zkts6jS3VfE/J0Kt1VxdFqTnMc3QgO5lFE5GKN3KDI/uUpSyK3CjQHmfEkYR4wCOl0R0XrsjpxfWEA++XA==} + '@next/swc-linux-arm64-gnu@15.5.7': + resolution: {integrity: sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.5.4': - resolution: {integrity: sha512-TOK7iTxmXFc45UrtKqWdZ1shfxuL4tnVAOuuJK4S88rX3oyVV4ZkLjtMT85wQkfBrOOvU55aLty+MV8xmcJR8A==} + '@next/swc-linux-arm64-musl@15.5.7': + resolution: {integrity: sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.5.4': - resolution: {integrity: sha512-7HKolaj+481FSW/5lL0BcTkA4Ueam9SPYWyN/ib/WGAFZf0DGAN8frNpNZYFHtM4ZstrHZS3LY3vrwlIQfsiMA==} + '@next/swc-linux-x64-gnu@15.5.7': + resolution: {integrity: sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.5.4': - resolution: {integrity: sha512-nlQQ6nfgN0nCO/KuyEUwwOdwQIGjOs4WNMjEUtpIQJPR2NUfmGpW2wkJln1d4nJ7oUzd1g4GivH5GoEPBgfsdw==} + '@next/swc-linux-x64-musl@15.5.7': + resolution: {integrity: sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.5.4': - resolution: {integrity: sha512-PcR2bN7FlM32XM6eumklmyWLLbu2vs+D7nJX8OAIoWy69Kef8mfiN4e8TUv2KohprwifdpFKPzIP1njuCjD0YA==} + '@next/swc-win32-arm64-msvc@15.5.7': + resolution: {integrity: sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.5.4': - resolution: {integrity: sha512-1ur2tSHZj8Px/KMAthmuI9FMp/YFusMMGoRNJaRZMOlSkgvLjzosSdQI0cJAKogdHl3qXUQKL9MGaYvKwA7DXg==} + '@next/swc-win32-x64-msvc@15.5.7': + resolution: {integrity: sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -2727,6 +2730,9 @@ packages: '@types/node@22.19.2': resolution: {integrity: sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==} + '@types/nodemailer@7.0.11': + resolution: {integrity: sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==} + '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -3636,8 +3642,8 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - eslint-config-next@15.5.4: - resolution: {integrity: sha512-BzgVVuT3kfJes8i2GHenC1SRJ+W3BTML11lAOYFOOPzrk2xp66jBOAGEFRw+3LkYCln5UzvFsLhojrshb5Zfaw==} + eslint-config-next@15.5.9: + resolution: {integrity: sha512-852JYI3NkFNzW8CqsMhI0K2CDRxTObdZ2jQJj5CtpEaOkYHn13107tHpNuD/h0WRpU4FAbCdUaxQsrfBtNK9Kw==} peerDependencies: eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 typescript: '>=3.3.1' @@ -4938,10 +4944,9 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@15.5.4: - resolution: {integrity: sha512-xH4Yjhb82sFYQfY3vbkJfgSDgXvBB6a8xPs9i35k6oZJRoQRihZH+4s9Yo2qsWpzBmZ3lPXaJ2KPXLfkvW4LnA==} + next@15.5.9: + resolution: {integrity: sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} - deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details. hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -4976,6 +4981,10 @@ packages: encoding: optional: true + nodemailer@8.0.2: + resolution: {integrity: sha512-zbj002pZAIkWQFxyAaqoxvn+zoIwRnS40hgjqTXudKOOJkiFFgBeNqjgD3/YCR12sZnrghWYBY+yP1ZucdDRpw==} + engines: {node: '>=6.0.0'} + normalize-package-data@6.0.2: resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==} engines: {node: ^16.14.0 || >=18.0.0} @@ -5515,10 +5524,10 @@ packages: peerDependencies: react: '>=16.8.0' - react-dom@19.2.0: - resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: - react: ^19.2.0 + react: ^19.2.4 react-hook-form@7.65.0: resolution: {integrity: sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==} @@ -5591,8 +5600,8 @@ packages: peerDependencies: react: '>=16.8.0 || ^17.0.0 || ^18' - react@19.2.0: - resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} read-package-up@11.0.0: @@ -6434,24 +6443,6 @@ packages: zod@4.1.12: resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} - zustand@5.0.8: - resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==} - engines: {node: '>=12.20.0'} - peerDependencies: - '@types/react': '>=18.0.0' - immer: '>=9.0.6' - react: '>=18.0.0' - use-sync-external-store: '>=1.2.0' - peerDependenciesMeta: - '@types/react': - optional: true - immer: - optional: true - react: - optional: true - use-sync-external-store: - optional: true - zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -6679,11 +6670,11 @@ snapshots: '@floating-ui/core': 1.7.3 '@floating-ui/utils': 0.2.10 - '@floating-ui/react-dom@2.1.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@floating-ui/react-dom@2.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@floating-ui/dom': 1.7.4 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) '@floating-ui/utils@0.2.10': {} @@ -6707,10 +6698,10 @@ snapshots: dependencies: hono: 4.7.10 - '@hookform/resolvers@5.2.2(react-hook-form@7.65.0(react@19.2.0))': + '@hookform/resolvers@5.2.2(react-hook-form@7.65.0(react@19.2.4))': dependencies: '@standard-schema/utils': 0.3.0 - react-hook-form: 7.65.0(react@19.2.0) + react-hook-form: 7.65.0(react@19.2.4) '@humanfs/core@0.19.1': {} @@ -6854,11 +6845,11 @@ snapshots: '@levischuck/tiny-cbor@0.2.11': {} - '@logtail/next@0.2.1(next@15.5.4(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)': + '@logtail/next@0.2.1(next@15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)': dependencies: - next: 15.5.4(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: 19.2.0 - use-deep-compare: 1.3.0(react@19.2.0) + next: 15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + use-deep-compare: 1.3.0(react@19.2.4) whatwg-fetch: 3.6.20 '@mrleebo/prisma-ast@0.12.1': @@ -6880,34 +6871,34 @@ snapshots: - bufferutil - utf-8-validate - '@next/env@15.5.4': {} + '@next/env@15.5.9': {} - '@next/eslint-plugin-next@15.5.4': + '@next/eslint-plugin-next@15.5.9': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@15.5.4': + '@next/swc-darwin-arm64@15.5.7': optional: true - '@next/swc-darwin-x64@15.5.4': + '@next/swc-darwin-x64@15.5.7': optional: true - '@next/swc-linux-arm64-gnu@15.5.4': + '@next/swc-linux-arm64-gnu@15.5.7': optional: true - '@next/swc-linux-arm64-musl@15.5.4': + '@next/swc-linux-arm64-musl@15.5.7': optional: true - '@next/swc-linux-x64-gnu@15.5.4': + '@next/swc-linux-x64-gnu@15.5.7': optional: true - '@next/swc-linux-x64-musl@15.5.4': + '@next/swc-linux-x64-musl@15.5.7': optional: true - '@next/swc-win32-arm64-msvc@15.5.4': + '@next/swc-win32-arm64-msvc@15.5.7': optional: true - '@next/swc-win32-x64-msvc@15.5.4': + '@next/swc-win32-x64-msvc@15.5.7': optional: true '@noble/ciphers@2.0.1': {} @@ -7780,11 +7771,11 @@ snapshots: '@prisma/client-runtime-utils@7.0.0': {} - '@prisma/client@7.0.0(prisma@7.0.0(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3))(typescript@5.9.3)': + '@prisma/client@7.0.0(prisma@7.0.0(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3)': dependencies: '@prisma/client-runtime-utils': 7.0.0 optionalDependencies: - prisma: 7.0.0(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) + prisma: 7.0.0(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) typescript: 5.9.3 '@prisma/config@7.0.0': @@ -7851,11 +7842,11 @@ snapshots: '@prisma/query-plan-executor@6.18.0': {} - '@prisma/studio-core-licensed@0.8.0(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@prisma/studio-core-licensed@0.8.0(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@types/react': 19.2.2 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) '@protobufjs/aspromise@1.1.2': {} @@ -7884,743 +7875,743 @@ snapshots: '@radix-ui/primitive@1.1.3': {} - '@radix-ui/react-accessible-icon@1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-accessible-icon@1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-aspect-ratio@1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-aspect-ratio@1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-avatar@1.1.10(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-avatar@1.1.10(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.2)(react@19.2.0)': + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.2)(react@19.2.4)': dependencies: - react: 19.2.0 + react: 19.2.4 optionalDependencies: '@types/react': 19.2.2 - '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-context@1.1.2(@types/react@19.2.2)(react@19.2.0)': + '@radix-ui/react-context@1.1.2(@types/react@19.2.2)(react@19.2.4)': dependencies: - react: 19.2.0 + react: 19.2.4 optionalDependencies: '@types/react': 19.2.2 - '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.4) aria-hidden: 1.2.6 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - react-remove-scroll: 2.7.1(@types/react@19.2.2)(react@19.2.0) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.1(@types/react@19.2.2)(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-direction@1.1.1(@types/react@19.2.2)(react@19.2.0)': + '@radix-ui/react-direction@1.1.1(@types/react@19.2.2)(react@19.2.4)': dependencies: - react: 19.2.0 + react: 19.2.4 optionalDependencies: '@types/react': 19.2.2 - '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.2)(react@19.2.0)': + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.2)(react@19.2.4)': dependencies: - react: 19.2.0 + react: 19.2.4 optionalDependencies: '@types/react': 19.2.2 - '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-form@0.1.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-form@0.1.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-id@1.1.1(@types/react@19.2.2)(react@19.2.0)': + '@radix-ui/react-id@1.1.1(@types/react@19.2.2)(react@19.2.4)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 optionalDependencies: '@types/react': 19.2.2 - '@radix-ui/react-label@2.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-label@2.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.4) aria-hidden: 1.2.6 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - react-remove-scroll: 2.7.1(@types/react@19.2.2)(react@19.2.0) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.1(@types/react@19.2.2)(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-menubar@1.1.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-menubar@1.1.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-one-time-password-field@0.1.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-one-time-password-field@0.1.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-password-toggle-field@0.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-password-toggle-field@0.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.4) aria-hidden: 1.2.6 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - react-remove-scroll: 2.7.1(@types/react@19.2.2)(react@19.2.0) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.1(@types/react@19.2.2)(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@floating-ui/react-dom': 2.1.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@floating-ui/react-dom': 2.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.2)(react@19.2.4) '@radix-ui/rect': 1.1.1 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-progress@1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-progress@1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) aria-hidden: 1.2.6 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - react-remove-scroll: 2.7.1(@types/react@19.2.2)(react@19.2.0) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.1(@types/react@19.2.2)(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-separator@1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-separator@1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-slot@1.2.3(@types/react@19.2.2)(react@19.2.0)': + '@radix-ui/react-slot@1.2.3(@types/react@19.2.2)(react@19.2.4)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 optionalDependencies: '@types/react': 19.2.2 - '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-toast@1.2.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-toast@1.2.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-toolbar@1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-toolbar@1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) - '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.2)(react@19.2.0)': + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.2)(react@19.2.4)': dependencies: - react: 19.2.0 + react: 19.2.4 optionalDependencies: '@types/react': 19.2.2 - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.2)(react@19.2.0)': + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.2)(react@19.2.4)': dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 optionalDependencies: '@types/react': 19.2.2 - '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.2)(react@19.2.0)': + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.2)(react@19.2.4)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 optionalDependencies: '@types/react': 19.2.2 - '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.2)(react@19.2.0)': + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.2)(react@19.2.4)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 optionalDependencies: '@types/react': 19.2.2 - '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.2)(react@19.2.0)': + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.2)(react@19.2.4)': dependencies: - react: 19.2.0 - use-sync-external-store: 1.6.0(react@19.2.0) + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.2)(react@19.2.0)': + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.2)(react@19.2.4)': dependencies: - react: 19.2.0 + react: 19.2.4 optionalDependencies: '@types/react': 19.2.2 - '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.2)(react@19.2.0)': + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.2)(react@19.2.4)': dependencies: - react: 19.2.0 + react: 19.2.4 optionalDependencies: '@types/react': 19.2.2 - '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.2)(react@19.2.0)': + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.2)(react@19.2.4)': dependencies: '@radix-ui/rect': 1.1.1 - react: 19.2.0 + react: 19.2.4 optionalDependencies: '@types/react': 19.2.2 - '@radix-ui/react-use-size@1.1.1(@types/react@19.2.2)(react@19.2.0)': + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.2)(react@19.2.4)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) - react: 19.2.0 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.4) + react: 19.2.4 optionalDependencies: '@types/react': 19.2.2 - '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) @@ -8769,10 +8760,10 @@ snapshots: valibot: 1.1.0(typescript@5.9.3) zod: 3.25.76 - '@tabler/icons-react@3.35.0(react@19.2.0)': + '@tabler/icons-react@3.35.0(react@19.2.4)': dependencies: '@tabler/icons': 3.35.0 - react: 19.2.0 + react: 19.2.4 '@tabler/icons@3.35.0': {} @@ -8855,16 +8846,16 @@ snapshots: '@tanstack/query-core@5.90.2': {} - '@tanstack/react-query@5.90.2(react@19.2.0)': + '@tanstack/react-query@5.90.2(react@19.2.4)': dependencies: '@tanstack/query-core': 5.90.2 - react: 19.2.0 + react: 19.2.4 - '@tanstack/react-table@8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@tanstack/react-table@8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@tanstack/table-core': 8.21.3 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) '@tanstack/table-core@8.21.3': {} @@ -8873,13 +8864,13 @@ snapshots: '@trpc/server': 11.6.0(typescript@5.9.3) typescript: 5.9.3 - '@trpc/react-query@11.6.0(@tanstack/react-query@5.90.2(react@19.2.0))(@trpc/client@11.6.0(@trpc/server@11.6.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)': + '@trpc/react-query@11.6.0(@tanstack/react-query@5.90.2(react@19.2.4))(@trpc/client@11.6.0(@trpc/server@11.6.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.6.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)': dependencies: - '@tanstack/react-query': 5.90.2(react@19.2.0) + '@tanstack/react-query': 5.90.2(react@19.2.4) '@trpc/client': 11.6.0(@trpc/server@11.6.0(typescript@5.9.3))(typescript@5.9.3) '@trpc/server': 11.6.0(typescript@5.9.3) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) typescript: 5.9.3 '@trpc/server@11.6.0(typescript@5.9.3)': @@ -8977,11 +8968,11 @@ snapshots: dependencies: '@tsparticles/engine': 3.9.1 - '@tsparticles/react@3.0.0(@tsparticles/engine@3.9.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@tsparticles/react@3.0.0(@tsparticles/engine@3.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@tsparticles/engine': 3.9.1 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) '@tsparticles/shape-circle@3.9.1': dependencies: @@ -9162,6 +9153,10 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/nodemailer@7.0.11': + dependencies: + '@types/node': 20.19.21 + '@types/normalize-package-data@2.4.4': {} '@types/oracledb@6.5.2': @@ -9529,7 +9524,7 @@ snapshots: before-after-hook@4.0.0: {} - better-auth@1.3.27(next@15.5.4(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + better-auth@1.3.27(next@15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@better-auth/core': 1.3.27 '@better-auth/utils': 0.3.0 @@ -9545,9 +9540,9 @@ snapshots: nanostores: 1.0.1 zod: 4.1.12 optionalDependencies: - next: 15.5.4(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + next: 15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) better-call@1.0.19: dependencies: @@ -9714,14 +9709,14 @@ snapshots: clsx@2.1.1: {} - cmdk@1.1.1(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + cmdk@1.1.1(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) transitivePeerDependencies: - '@types/react' - '@types/react-dom' @@ -9973,11 +9968,11 @@ snapshots: '@standard-schema/spec': 1.0.0 fast-check: 3.23.2 - embla-carousel-react@8.6.0(react@19.2.0): + embla-carousel-react@8.6.0(react@19.2.4): dependencies: embla-carousel: 8.6.0 embla-carousel-reactive-utils: 8.6.0(embla-carousel@8.6.0) - react: 19.2.0 + react: 19.2.4 embla-carousel-reactive-utils@8.6.0(embla-carousel@8.6.0): dependencies: @@ -10153,9 +10148,9 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-next@15.5.4(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3): + eslint-config-next@15.5.9(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@next/eslint-plugin-next': 15.5.4 + '@next/eslint-plugin-next': 15.5.9 '@rushstack/eslint-patch': 1.13.0 '@typescript-eslint/eslint-plugin': 8.46.0(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/parser': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) @@ -10487,14 +10482,14 @@ snapshots: forwarded-parse@2.1.2: {} - framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + framer-motion@12.23.24(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: motion-dom: 12.23.23 motion-utils: 12.23.6 tslib: 2.8.1 optionalDependencies: - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) from2@2.3.0: dependencies: @@ -10879,7 +10874,7 @@ snapshots: inline-style-parser@0.2.4: {} - inngest@3.47.0(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(hono@4.7.10)(next@15.5.4(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@5.9.3)(zod@3.25.76): + inngest@3.47.0(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(hono@4.7.10)(next@15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(zod@3.25.76): dependencies: '@bufbuild/protobuf': 2.10.2 '@inngest/ai': 0.1.7 @@ -10908,17 +10903,17 @@ snapshots: zod: 3.25.76 optionalDependencies: hono: 4.7.10 - next: 15.5.4(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + next: 15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) typescript: 5.9.3 transitivePeerDependencies: - '@opentelemetry/core' - encoding - supports-color - input-otp@1.4.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + input-otp@1.4.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) internal-slot@1.1.0: dependencies: @@ -11293,9 +11288,9 @@ snapshots: lru.min@1.1.3: {} - lucide-react@0.545.0(react@19.2.0): + lucide-react@0.545.0(react@19.2.4): dependencies: - react: 19.2.0 + react: 19.2.4 magic-string@0.30.19: dependencies: @@ -11709,13 +11704,13 @@ snapshots: motion-utils@12.23.6: {} - motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + motion@12.23.24(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - framer-motion: 12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + framer-motion: 12.23.24(react-dom@19.2.4(react@19.2.4))(react@19.2.4) tslib: 2.8.1 optionalDependencies: - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) mrmime@2.0.1: {} @@ -11757,29 +11752,29 @@ snapshots: nerf-dart@1.0.0: {} - next-themes@0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) - next@15.5.4(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + next@15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@next/env': 15.5.4 + '@next/env': 15.5.9 '@swc/helpers': 0.5.15 caniuse-lite: 1.0.30001750 postcss: 8.4.31 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - styled-jsx: 5.1.6(react@19.2.0) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + styled-jsx: 5.1.6(react@19.2.4) optionalDependencies: - '@next/swc-darwin-arm64': 15.5.4 - '@next/swc-darwin-x64': 15.5.4 - '@next/swc-linux-arm64-gnu': 15.5.4 - '@next/swc-linux-arm64-musl': 15.5.4 - '@next/swc-linux-x64-gnu': 15.5.4 - '@next/swc-linux-x64-musl': 15.5.4 - '@next/swc-win32-arm64-msvc': 15.5.4 - '@next/swc-win32-x64-msvc': 15.5.4 + '@next/swc-darwin-arm64': 15.5.7 + '@next/swc-darwin-x64': 15.5.7 + '@next/swc-linux-arm64-gnu': 15.5.7 + '@next/swc-linux-arm64-musl': 15.5.7 + '@next/swc-linux-x64-gnu': 15.5.7 + '@next/swc-linux-x64-musl': 15.5.7 + '@next/swc-win32-arm64-msvc': 15.5.7 + '@next/swc-win32-x64-msvc': 15.5.7 '@opentelemetry/api': 1.9.0 '@playwright/test': 1.56.1 sharp: 0.34.5 @@ -11800,6 +11795,8 @@ snapshots: dependencies: whatwg-url: 5.0.0 + nodemailer@8.0.2: {} + normalize-package-data@6.0.2: dependencies: hosted-git-info: 7.0.2 @@ -12113,12 +12110,12 @@ snapshots: dependencies: parse-ms: 4.0.0 - prisma@7.0.0(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3): + prisma@7.0.0(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3): dependencies: '@prisma/config': 7.0.0 '@prisma/dev': 0.13.0(typescript@5.9.3) '@prisma/engines': 7.0.0 - '@prisma/studio-core-licensed': 0.8.0(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@prisma/studio-core-licensed': 0.8.0(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) mysql2: 3.15.3 postgres: 3.4.7 optionalDependencies: @@ -12183,65 +12180,65 @@ snapshots: queue-microtask@1.2.3: {} - radix-ui@1.4.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + radix-ui@1.4.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-accessible-icon': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-alert-dialog': 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-aspect-ratio': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-avatar': 1.1.10(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-checkbox': 1.3.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-context-menu': 2.2.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-form': 0.1.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-hover-card': 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-menubar': 1.1.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-navigation-menu': 1.2.14(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-one-time-password-field': 0.1.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-password-toggle-field': 0.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-progress': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-radio-group': 1.3.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-select': 2.2.6(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-slider': 1.3.6(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-switch': 1.2.6(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-toast': 1.2.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-toolbar': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-accessible-icon': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-alert-dialog': 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-aspect-ratio': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-avatar': 1.1.10(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-checkbox': 1.3.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-context-menu': 2.2.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-form': 0.1.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-hover-card': 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-menubar': 1.1.16(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-navigation-menu': 1.2.14(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-one-time-password-field': 0.1.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-password-toggle-field': 0.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-progress': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-radio-group': 1.3.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-select': 2.2.6(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slider': 1.3.6(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-switch': 1.2.6(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toast': 1.2.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toolbar': 1.1.11(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.2)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 '@types/react-dom': 19.2.1(@types/react@19.2.2) @@ -12258,27 +12255,27 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 - react-day-picker@9.11.3(react@19.2.0): + react-day-picker@9.11.3(react@19.2.4): dependencies: '@date-fns/tz': 1.4.1 date-fns: 4.1.0 date-fns-jalali: 4.1.0-0 - react: 19.2.0 + react: 19.2.4 - react-dom@19.2.0(react@19.2.0): + react-dom@19.2.4(react@19.2.4): dependencies: - react: 19.2.0 + react: 19.2.4 scheduler: 0.27.0 - react-hook-form@7.65.0(react@19.2.0): + react-hook-form@7.65.0(react@19.2.4): dependencies: - react: 19.2.0 + react: 19.2.4 react-is@16.13.1: {} react-is@18.3.1: {} - react-markdown@10.1.0(@types/react@19.2.2)(react@19.2.0): + react-markdown@10.1.0(@types/react@19.2.2)(react@19.2.4): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 @@ -12287,7 +12284,7 @@ snapshots: hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 mdast-util-to-hast: 13.2.0 - react: 19.2.0 + react: 19.2.4 remark-parse: 11.0.0 remark-rehype: 11.1.2 unified: 11.0.5 @@ -12296,60 +12293,60 @@ snapshots: transitivePeerDependencies: - supports-color - react-remove-scroll-bar@2.3.8(@types/react@19.2.2)(react@19.2.0): + react-remove-scroll-bar@2.3.8(@types/react@19.2.2)(react@19.2.4): dependencies: - react: 19.2.0 - react-style-singleton: 2.2.3(@types/react@19.2.2)(react@19.2.0) + react: 19.2.4 + react-style-singleton: 2.2.3(@types/react@19.2.2)(react@19.2.4) tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.2 - react-remove-scroll@2.7.1(@types/react@19.2.2)(react@19.2.0): + react-remove-scroll@2.7.1(@types/react@19.2.2)(react@19.2.4): dependencies: - react: 19.2.0 - react-remove-scroll-bar: 2.3.8(@types/react@19.2.2)(react@19.2.0) - react-style-singleton: 2.2.3(@types/react@19.2.2)(react@19.2.0) + react: 19.2.4 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.2)(react@19.2.4) + react-style-singleton: 2.2.3(@types/react@19.2.2)(react@19.2.4) tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@19.2.2)(react@19.2.0) - use-sidecar: 1.1.3(@types/react@19.2.2)(react@19.2.0) + use-callback-ref: 1.3.3(@types/react@19.2.2)(react@19.2.4) + use-sidecar: 1.1.3(@types/react@19.2.2)(react@19.2.4) optionalDependencies: '@types/react': 19.2.2 - react-resizable-panels@3.0.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + react-resizable-panels@3.0.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) - react-smooth@4.0.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + react-smooth@4.0.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: fast-equals: 5.3.2 prop-types: 15.8.1 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - react-transition-group: 4.4.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-transition-group: 4.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react-style-singleton@2.2.3(@types/react@19.2.2)(react@19.2.0): + react-style-singleton@2.2.3(@types/react@19.2.2)(react@19.2.4): dependencies: get-nonce: 1.0.1 - react: 19.2.0 + react: 19.2.4 tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.2 - react-transition-group@4.4.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + react-transition-group@4.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@babel/runtime': 7.28.4 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) - react-wrap-balancer@1.1.1(react@19.2.0): + react-wrap-balancer@1.1.1(react@19.2.4): dependencies: - react: 19.2.0 + react: 19.2.4 - react@19.2.0: {} + react@19.2.4: {} read-package-up@11.0.0: dependencies: @@ -12381,15 +12378,15 @@ snapshots: dependencies: decimal.js-light: 2.5.1 - recharts@2.15.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + recharts@2.15.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: clsx: 2.1.1 eventemitter3: 4.0.7 lodash: 4.17.21 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) react-is: 18.3.1 - react-smooth: 4.0.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react-smooth: 4.0.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) recharts-scale: 0.4.5 tiny-invariant: 1.3.3 victory-vendor: 36.9.2 @@ -12728,10 +12725,10 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 - sonner@2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) source-map-js@1.2.1: {} @@ -12887,10 +12884,10 @@ snapshots: dependencies: inline-style-parser: 0.2.4 - styled-jsx@5.1.6(react@19.2.0): + styled-jsx@5.1.6(react@19.2.4): dependencies: client-only: 0.0.1 - react: 19.2.0 + react: 19.2.4 super-regex@1.0.0: dependencies: @@ -13174,29 +13171,29 @@ snapshots: url-join@5.0.0: {} - use-callback-ref@1.3.3(@types/react@19.2.2)(react@19.2.0): + use-callback-ref@1.3.3(@types/react@19.2.2)(react@19.2.4): dependencies: - react: 19.2.0 + react: 19.2.4 tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.2 - use-deep-compare@1.3.0(react@19.2.0): + use-deep-compare@1.3.0(react@19.2.4): dependencies: dequal: 2.0.3 - react: 19.2.0 + react: 19.2.4 - use-sidecar@1.1.3(@types/react@19.2.2)(react@19.2.0): + use-sidecar@1.1.3(@types/react@19.2.2)(react@19.2.4): dependencies: detect-node-es: 1.1.0 - react: 19.2.0 + react: 19.2.4 tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.2 - use-sync-external-store@1.6.0(react@19.2.0): + use-sync-external-store@1.6.0(react@19.2.4): dependencies: - react: 19.2.0 + react: 19.2.4 util-deprecate@1.0.2: {} @@ -13211,11 +13208,11 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - vaul@1.1.2(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + vaul@1.1.2(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) transitivePeerDependencies: - '@types/react' - '@types/react-dom' @@ -13402,10 +13399,4 @@ snapshots: zod@4.1.12: {} - zustand@5.0.8(@types/react@19.2.2)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)): - optionalDependencies: - '@types/react': 19.2.2 - react: 19.2.0 - use-sync-external-store: 1.6.0(react@19.2.0) - zwitch@2.0.4: {} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1a60169..87095c5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -158,6 +158,7 @@ model BankAccount { recurringRules RecurringRule[] transactions Transaction[] + @@unique([userId, name]) @@index([userId]) @@map("accounts") } @@ -287,7 +288,7 @@ model Transaction { paymentMethod PaymentMethod? account BankAccount @relation(fields: [accountId], references: [id], onDelete: Cascade) category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull) - recurringRule RecurringRule? @relation(fields: [recurringRuleId], references: [id]) + recurringRule RecurringRule? @relation(fields: [recurringRuleId], references: [id], onDelete: SetNull) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@ -295,6 +296,8 @@ model Transaction { @@index([categoryId]) @@index([date]) @@index([userId, date]) + @@index([userId, accountId]) + @@index([recurringRuleId]) @@map("transactions") } diff --git a/src/app/(auth)/error.tsx b/src/app/(auth)/error.tsx new file mode 100644 index 0000000..45e8602 --- /dev/null +++ b/src/app/(auth)/error.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { Button } from "@ui/button"; +import { AlertTriangle } from "lucide-react"; + +export default function AuthError({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + const router = useRouter(); + + return ( +
+
+
+ +
+

Authentication Error

+

+ {error.message || "Something went wrong during authentication."} +

+
+ + +
+
+
+ ); +} diff --git a/src/app/(auth)/reset-password/page.tsx b/src/app/(auth)/reset-password/page.tsx index 9c5188d..f2f40b6 100644 --- a/src/app/(auth)/reset-password/page.tsx +++ b/src/app/(auth)/reset-password/page.tsx @@ -3,14 +3,9 @@ import { useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { useAuth } from "@/hooks/use-auth"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import { - Field, - FieldGroup, - FieldLabel, - FieldDescription, -} from "@/components/ui/field"; +import { Input } from "@ui/input"; +import { Button } from "@ui/button"; +import { Field, FieldGroup, FieldLabel, FieldDescription } from "@ui/field"; import { toast } from "sonner"; import { LockKeyholeOpen } from "lucide-react"; diff --git a/src/app/(auth)/verify-success/page.tsx b/src/app/(auth)/verify-success/page.tsx index 8d6cc47..09ca01b 100644 --- a/src/app/(auth)/verify-success/page.tsx +++ b/src/app/(auth)/verify-success/page.tsx @@ -1,7 +1,7 @@ "use client"; import { CheckCircle2 } from "lucide-react"; -import { Button } from "@/components/ui/button"; +import { Button } from "@ui/button"; import { useRouter } from "next/navigation"; export default function VerifySuccessPage() { diff --git a/src/app/(features)/accounts/[id]/_client.tsx b/src/app/(features)/accounts/[id]/_client.tsx index 9994dbe..62ace4f 100644 --- a/src/app/(features)/accounts/[id]/_client.tsx +++ b/src/app/(features)/accounts/[id]/_client.tsx @@ -1,17 +1,17 @@ "use client"; -import React, { useState, useCallback } from "react"; +import React, { Suspense, useState, useCallback } from "react"; import dynamic from "next/dynamic"; import { useParams } from "next/navigation"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { Card, CardContent } from "@/components/ui/card"; -import { ICON_MAP } from "@/components/common/icon-picker"; +import { Button } from "@ui/button"; +import { Badge } from "@ui/badge"; +import { Card, CardContent } from "@ui/card"; +import { ICON_MAP } from "@/constants/icons"; import { PlusCircle, Wallet } from "lucide-react"; import { useAccounts } from "@/hooks/use-accounts"; import { useTransactions } from "@/hooks/use-transactions"; import { useFormatter } from "@/hooks/use-formatter"; -import { TransactionsTable } from "@/components/common/transactions-table"; +import { TransactionsTable } from "@common/transactions-table"; const TransactionForm = dynamic( () => import("@/components/forms/transaction/transaction-form"), @@ -38,6 +38,28 @@ export default function AccountDetailPageClient() { selected: Transaction | null; }>({ open: false, selected: null }); + const handleOpenTxDialog = useCallback(() => { + setTxDialog((prev) => ({ ...prev, open: true })); + }, []); + + const handleTxDialogOpenChange = useCallback((open: boolean) => { + setTxDialog((prev) => ({ + open, + selected: open ? prev.selected : null, + })); + }, []); + + const handleEditTx = useCallback((transaction: Transaction) => { + setTxDialog({ open: true, selected: transaction }); + }, []); + + const handleDeleteTx = useCallback( + async (ids: string[]) => { + await Promise.all(ids.map((id) => remove.mutateAsync({ id }))); + }, + [remove], + ); + const handlePageChange = useCallback( (newPageIndex: number, newPageSize?: number) => { setPagination((prev) => { @@ -124,9 +146,7 @@ export default function AccountDetailPageClient() {

- @@ -134,24 +154,21 @@ export default function AccountDetailPageClient() { - { - setTxDialog((prev) => ({ - open, - selected: open ? prev.selected : null, - })); - }} - accountId={account.id} - initialValues={ - txDialog.selected - ? { - ...txDialog.selected, - paymentMethod: txDialog.selected.paymentMethod ?? undefined, - } - : null - } - /> + + + { - setTxDialog({ open: true, selected: transaction }); - }} - onDelete={async (ids) => { - await Promise.all(ids.map((id) => remove.mutateAsync({ id }))); - }} - onView={(transaction) => { - setTxDialog({ open: true, selected: transaction }); - }} + onEdit={handleEditTx} + onDelete={handleDeleteTx} + onView={handleEditTx} accountId={account.id} /> diff --git a/src/app/(features)/accounts/[id]/loading.tsx b/src/app/(features)/accounts/[id]/loading.tsx index da0637f..bb92f6d 100644 --- a/src/app/(features)/accounts/[id]/loading.tsx +++ b/src/app/(features)/accounts/[id]/loading.tsx @@ -1,5 +1,5 @@ -import { Skeleton } from "@/components/ui/skeleton"; -import { TableSkeleton } from "@/components/skeletons/table-skeleton"; +import { Skeleton } from "@ui/skeleton"; +import { TableSkeleton } from "@skeletons/table-skeleton"; export default function AccountDetailLoading() { return ( diff --git a/src/app/(features)/accounts/_client.tsx b/src/app/(features)/accounts/_client.tsx index e9cec08..36de46a 100644 --- a/src/app/(features)/accounts/_client.tsx +++ b/src/app/(features)/accounts/_client.tsx @@ -1,18 +1,18 @@ "use client"; -import React, { useState, useCallback } from "react"; +import React, { Suspense, useState, useCallback } from "react"; import dynamic from "next/dynamic"; import { useRouter } from "next/navigation"; import { Wallet, PlusCircle } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { Skeleton } from "@/components/ui/skeleton"; +import { Button } from "@ui/button"; +import { Skeleton } from "@ui/skeleton"; const AccountForm = dynamic( () => import("@/components/forms/accounts/account-form"), { ssr: false, loading: () => null }, ); -import { DeleteDialog } from "@/components/common/delete-dialog"; +import { DeleteDialog } from "@common/delete-dialog"; import { useAccounts } from "@/hooks/use-accounts"; import { useFormatter } from "@/hooks/use-formatter"; import type { BankAccount as Account } from "@/types/account"; @@ -43,6 +43,26 @@ export default function AccountsPageClient() { deletingId: string | null; }>({ isCreateOpen: false, editingAccount: null, deletingId: null }); + const handleOpenCreate = useCallback(() => { + setModalState((prev) => ({ ...prev, isCreateOpen: true })); + }, []); + + const handleCreateOpenChange = useCallback((open: boolean) => { + setModalState((prev) => ({ ...prev, isCreateOpen: open })); + }, []); + + const handleEditOpenChange = useCallback((open: boolean) => { + if (!open) { + setModalState((prev) => ({ ...prev, editingAccount: null })); + } + }, []); + + const handleDeleteOpenChange = useCallback((open: boolean) => { + if (!open) { + setModalState((prev) => ({ ...prev, deletingId: null })); + } + }, []); + const handleEdit = useCallback( (e: React.MouseEvent, account: Account | ApiAccount) => { e.stopPropagation(); @@ -80,12 +100,7 @@ export default function AccountsPageClient() {
{accounts.length > 0 && ( - @@ -97,11 +112,7 @@ export default function AccountsPageClient() { {isLoading ? ( ) : accounts.length === 0 ? ( - - setModalState((prev) => ({ ...prev, isCreateOpen: true })) - } - /> + ) : (
{accounts.map((account) => ( @@ -118,29 +129,26 @@ export default function AccountsPageClient() { )}
- - setModalState((prev) => ({ ...prev, isCreateOpen: open })) - } - /> - - {modalState.editingAccount && ( + - !open && - setModalState((prev) => ({ ...prev, editingAccount: null })) - } - initialValues={mapToInitialValues(modalState.editingAccount)} + open={modalState.isCreateOpen} + onOpenChange={handleCreateOpenChange} /> + + + {modalState.editingAccount && ( + + + )} - !open && setModalState((prev) => ({ ...prev, deletingId: null })) - } + onOpenChange={handleDeleteOpenChange} title="Delete Account" description="Are you sure you want to delete this account? All associated transactions will be permanently removed." confirmText="Delete Account" diff --git a/src/app/(features)/accounts/loading.tsx b/src/app/(features)/accounts/loading.tsx index b448410..94abe34 100644 --- a/src/app/(features)/accounts/loading.tsx +++ b/src/app/(features)/accounts/loading.tsx @@ -1,4 +1,4 @@ -import { AccountsSkeleton } from "@/components/skeletons/accounts-skeleton"; +import { AccountsSkeleton } from "@skeletons/accounts-skeleton"; export default function AccountsLoading() { return ; diff --git a/src/app/(features)/analytics/_client.tsx b/src/app/(features)/analytics/_client.tsx index d952b28..5059caf 100644 --- a/src/app/(features)/analytics/_client.tsx +++ b/src/app/(features)/analytics/_client.tsx @@ -1,10 +1,10 @@ "use client"; -import React, { useMemo, useState } from "react"; +import React, { Suspense, useMemo, useState } from "react"; import dynamic from "next/dynamic"; import { useTransactions } from "@/hooks/use-transactions"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Skeleton } from "@ui/skeleton"; +import { Card, CardContent, CardHeader, CardTitle } from "@ui/card"; const AreaChart = dynamic( () => @@ -19,8 +19,8 @@ import { SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select"; -import type { ChartConfig } from "@/components/ui/chart"; +} from "@ui/select"; +import type { ChartConfig } from "@ui/chart"; import { subMonths, format } from "date-fns"; import { useFormatter } from "@/hooks/use-formatter"; import { CalendarDays } from "lucide-react"; @@ -107,15 +107,19 @@ export default function AnalyticsPageClient() { - formatAmount(val)} - /> + } + > + formatAmount(val)} + /> +
diff --git a/src/app/(features)/analytics/loading.tsx b/src/app/(features)/analytics/loading.tsx index 257164a..2b0699c 100644 --- a/src/app/(features)/analytics/loading.tsx +++ b/src/app/(features)/analytics/loading.tsx @@ -1,4 +1,4 @@ -import { ChartSkeleton } from "@/components/skeletons/chart-skeleton"; +import { ChartSkeleton } from "@skeletons/chart-skeleton"; export default function AnalyticsLoading() { return ( diff --git a/src/app/(features)/budget/_client.tsx b/src/app/(features)/budget/_client.tsx index 4ca0754..fd85091 100644 --- a/src/app/(features)/budget/_client.tsx +++ b/src/app/(features)/budget/_client.tsx @@ -3,12 +3,12 @@ import React, { Suspense } from "react"; import dynamic from "next/dynamic"; import { api } from "@/trpc/react"; -import { toNum } from "@/lib/shared/decimal"; +import { toNum } from "@shared/decimal"; import { Loader2, Wallet } from "lucide-react"; import { BudgetCard } from "@/components/pages/(protected)/budget/budget-card"; import { CreateBudgetDialog } from "@/components/pages/(protected)/budget/create-budget-dialog"; -import { Skeleton } from "@/components/ui/skeleton"; -import type { ChartConfig } from "@/components/ui/chart"; +import { Skeleton } from "@ui/skeleton"; +import type { ChartConfig } from "@ui/chart"; const GenericRadarChart = dynamic( () => diff --git a/src/app/(features)/budget/loading.tsx b/src/app/(features)/budget/loading.tsx index 9406a39..bf4035e 100644 --- a/src/app/(features)/budget/loading.tsx +++ b/src/app/(features)/budget/loading.tsx @@ -1,4 +1,4 @@ -import { BudgetSkeleton } from "@/components/skeletons/budget-skeleton"; +import { BudgetSkeleton } from "@skeletons/budget-skeleton"; export default function BudgetLoading() { return ; diff --git a/src/app/(features)/error.tsx b/src/app/(features)/error.tsx new file mode 100644 index 0000000..a6d26e1 --- /dev/null +++ b/src/app/(features)/error.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { Button } from "@ui/button"; +import { AlertTriangle } from "lucide-react"; + +export default function FeaturesError({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + const router = useRouter(); + + return ( +
+
+
+ +
+

Something went wrong

+

+ {error.message || "An unexpected error occurred. Please try again."} +

+
+ + +
+
+
+ ); +} diff --git a/src/app/(features)/layout.tsx b/src/app/(features)/layout.tsx index 6f6310f..c30e44c 100644 --- a/src/app/(features)/layout.tsx +++ b/src/app/(features)/layout.tsx @@ -1,5 +1,5 @@ -import NavBar from "@/components/layout/navbar"; -import DashboardHeader from "@/components/layout/dashboard-header"; +import NavBar from "@common/layout/navbar"; +import DashboardHeader from "@common/layout/dashboard-header"; import { OnboardingGuard } from "@/components/pages/(protected)/onboarding/onboarding-guard"; export default function FeaturesLayout({ diff --git a/src/app/(features)/overview/_client.tsx b/src/app/(features)/overview/_client.tsx index b807805..9587216 100644 --- a/src/app/(features)/overview/_client.tsx +++ b/src/app/(features)/overview/_client.tsx @@ -1,62 +1,27 @@ "use client"; -import React, { Suspense, useMemo, useCallback, useState } from "react"; -import dynamic from "next/dynamic"; +import React, { useMemo, useCallback, useState } from "react"; import { api } from "@/trpc/react"; -import { invalidateTransactions } from "@/lib/trpc/invalidation"; +import { invalidateTransactions } from "@/trpc/invalidation"; import { useAccounts } from "@/hooks/use-accounts"; import { useTransactions } from "@/hooks/use-transactions"; import { useCategories } from "@/hooks/use-categories"; +import { useOverviewStats } from "./_hooks/use-overview-stats"; +import { useBarChartData } from "./_hooks/use-bar-chart-data"; +import { usePieChartData } from "./_hooks/use-pie-chart-data"; import { StatsCards } from "@/components/pages/(protected)/overview/stats-cards"; import { RecentTransactions } from "@/components/pages/(protected)/overview/recent-transactions"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; - -const BarChart = dynamic( - () => - import("@/components/charts/bar-chart").then((m) => ({ - default: m.BarChart, - })), - { loading: () => }, -); -const PieChart = dynamic( - () => - import("@/components/charts/pie-chart").then((m) => ({ - default: m.PieChart, - })), - { loading: () => }, -); import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { subMonths, format, startOfMonth, endOfMonth } from "date-fns"; -import type { ChartConfig } from "@/components/ui/chart"; -import type { Transaction } from "@/types/transaction"; + SpendingOverviewCard, + type DateRange, +} from "@/components/pages/(protected)/overview/spending-overview-card"; +import { SpendingByCategoryCard } from "@/components/pages/(protected)/overview/spending-by-category-card"; import { DefaultView } from "@prisma/client"; import { useRouter } from "next/navigation"; import { useSettings } from "@/hooks/use-settings"; import { useEffect } from "react"; import { useFormatter } from "@/hooks/use-formatter"; -import { CalendarDays } from "lucide-react"; - -const barChartConfig = { - amount: { - label: "Expenses", - color: "var(--chart-1)", - }, -} satisfies ChartConfig; - -type DateRange = "3" | "6" | "12"; - -const DATE_RANGE_OPTIONS: { value: DateRange; label: string }[] = [ - { value: "3", label: "Last 3 months" }, - { value: "6", label: "Last 6 months" }, - { value: "12", label: "Last 12 months" }, -]; +import type { Transaction } from "@/types/transaction"; export default function OverviewPageClient() { const { formatAmount } = useFormatter(); @@ -107,155 +72,14 @@ export default function OverviewPageClient() { const rangeMonths = parseInt(barRange); - // Stats data: depends only on accounts and transactions - const statsData = useMemo(() => { - const now = new Date(); - const currentMonthStart = startOfMonth(now); - const currentMonthEnd = endOfMonth(now); - const prevMonthStart = startOfMonth(subMonths(now, 1)); - const prevMonthEnd = endOfMonth(subMonths(now, 1)); - - const totalBalance = accounts.reduce( - (acc, curr) => acc + parseFloat(curr.balance), - 0, - ); - let totalIncome = 0; - let totalExpense = 0; - let currentMonthIncome = 0; - let currentMonthExpense = 0; - let prevMonthIncome = 0; - let prevMonthExpense = 0; - let earliestDate: Date | null = null; - let latestDate: Date | null = null; - let earliestIncomeDate: Date | null = null; - let earliestExpenseDate: Date | null = null; - - for (const tx of transactions) { - const amount = Math.abs(parseFloat(tx.amount)); - const txDate = new Date(tx.date); - - if (!earliestDate || txDate < earliestDate) earliestDate = txDate; - if (!latestDate || txDate > latestDate) latestDate = txDate; - - if (tx.type === "CREDIT") { - totalIncome += amount; - if (!earliestIncomeDate || txDate < earliestIncomeDate) - earliestIncomeDate = txDate; - if (txDate >= currentMonthStart && txDate <= currentMonthEnd) - currentMonthIncome += amount; - if (txDate >= prevMonthStart && txDate <= prevMonthEnd) - prevMonthIncome += amount; - } else if (tx.type === "DEBIT") { - totalExpense += amount; - if (!earliestExpenseDate || txDate < earliestExpenseDate) - earliestExpenseDate = txDate; - if (txDate >= currentMonthStart && txDate <= currentMonthEnd) - currentMonthExpense += amount; - if (txDate >= prevMonthStart && txDate <= prevMonthEnd) - prevMonthExpense += amount; - } - } - - const formatDateRange = (start: Date | null, end: Date | null) => { - if (!start) return "No data yet"; - const endDate = end ?? now; - return `${format(start, "d MMM")} - ${format(endDate, "d MMM yyyy")}`; - }; - - const calcChange = (current: number, previous: number): number | null => { - if (previous === 0) return current > 0 ? 100 : null; - return Math.round(((current - previous) / previous) * 100); - }; - - return { - balance: { - title: "Total Balance", - dateRange: formatDateRange(earliestDate, latestDate), - value: totalBalance.toString(), - changePercent: calcChange( - totalBalance, - totalBalance - (currentMonthIncome - currentMonthExpense), - ), - changeLabel: "Last Month", - }, - income: { - title: "Total Income", - dateRange: formatDateRange(earliestIncomeDate, latestDate), - value: totalIncome.toString(), - changePercent: calcChange(currentMonthIncome, prevMonthIncome), - changeLabel: "Last Month", - }, - spending: { - title: "Total Spending", - dateRange: formatDateRange(earliestExpenseDate, latestDate), - value: totalExpense.toString(), - changePercent: calcChange(currentMonthExpense, prevMonthExpense), - changeLabel: "Last Month", - }, - }; - }, [accounts, transactions]); - - // Bar chart data: depends on transactions and rangeMonths - const barChartData = useMemo(() => { - const now = new Date(); - const barBucketKeys: string[] = []; - const barBucketLookup: Record = {}; - const barDisplayLabels: Record = {}; - - for (let i = rangeMonths - 1; i >= 0; i--) { - const month = subMonths(now, i); - const lookupKey = format(month, "yyyy-MM"); - const displayLabel = format(month, "MMM"); - barBucketKeys.push(lookupKey); - barBucketLookup[lookupKey] = 0; - barDisplayLabels[lookupKey] = displayLabel; - } - - for (const tx of transactions) { - if (tx.type === "DEBIT") { - const amount = Math.abs(parseFloat(tx.amount)); - const barKey = format(new Date(tx.date), "yyyy-MM"); - if (barKey in barBucketLookup) { - barBucketLookup[barKey]! += amount; - } - } - } - - return barBucketKeys.map((key) => ({ - date: barDisplayLabels[key]!, - amount: barBucketLookup[key]!, - })); - }, [transactions, rangeMonths]); - - // Pie chart data: depends on transactions, categoryMap, and category list - const pieChartData = useMemo(() => { - const categoryTotals: Record = {}; - - for (const tx of transactions) { - if (tx.type === "DEBIT") { - const amount = Math.abs(parseFloat(tx.amount)); - const catName = tx.categoryId - ? (categoryMap.get(tx.categoryId) ?? "Other") - : "Uncategorized"; - categoryTotals[catName] = (categoryTotals[catName] ?? 0) + amount; - } - } - - const categories = allFlat.data ?? []; - return Object.entries(categoryTotals).map(([name, value], index) => { - const category = categories.find((c) => c.name === name); - const fill = category?.color ?? `var(--chart-${(index % 5) + 1})`; - return { name, value, fill }; - }); - }, [transactions, categoryMap, allFlat.data]); - - const pieChartConfig = useMemo(() => { - const config: ChartConfig = {}; - for (const item of pieChartData) { - config[item.name] = { label: item.name, color: item.fill }; - } - return config; - }, [pieChartData]); + const statsData = useOverviewStats(accounts, transactions); + const barChartData = useBarChartData(transactions, rangeMonths); + const categories = useMemo(() => allFlat.data ?? [], [allFlat.data]); + const { pieChartData, pieChartConfig } = usePieChartData( + transactions, + categoryMap, + categories, + ); return (
@@ -267,61 +91,17 @@ export default function OverviewPageClient() { />
- - - Spending Overview - - - - } - > - formatAmount(val)} - /> - - - - - - - Spending by Category - - - } - > - formatAmount(val)} - /> - - - + +
{ + return useMemo(() => { + const now = new Date(); + const barBucketKeys: string[] = []; + const barBucketLookup: Record = {}; + const barDisplayLabels: Record = {}; + + for (let i = rangeMonths - 1; i >= 0; i--) { + const month = subMonths(now, i); + const lookupKey = format(month, "yyyy-MM"); + const displayLabel = format(month, "MMM"); + barBucketKeys.push(lookupKey); + barBucketLookup[lookupKey] = 0; + barDisplayLabels[lookupKey] = displayLabel; + } + + for (const tx of transactions) { + if (tx.type === "DEBIT") { + const amount = Math.abs(parseFloat(tx.amount)); + const barKey = format(new Date(tx.date), "yyyy-MM"); + if (barKey in barBucketLookup) { + barBucketLookup[barKey]! += amount; + } + } + } + + return barBucketKeys.map((key) => ({ + date: barDisplayLabels[key]!, + amount: barBucketLookup[key]!, + })); + }, [transactions, rangeMonths]); +} diff --git a/src/app/(features)/overview/_hooks/use-overview-stats.ts b/src/app/(features)/overview/_hooks/use-overview-stats.ts new file mode 100644 index 0000000..d04b025 --- /dev/null +++ b/src/app/(features)/overview/_hooks/use-overview-stats.ts @@ -0,0 +1,110 @@ +import { useMemo } from "react"; +import { startOfMonth, endOfMonth, subMonths, format } from "date-fns"; +import type { ApiBankAccount } from "@/types/account"; +import type { Transaction } from "@/types/transaction"; + +interface StatCard { + title: string; + dateRange: string; + value: string; + changePercent: number | null; + changeLabel: string; +} + +export interface OverviewStats { + balance: StatCard; + income: StatCard; + spending: StatCard; +} + +export function useOverviewStats( + accounts: ApiBankAccount[], + transactions: Transaction[], +): OverviewStats { + return useMemo(() => { + const now = new Date(); + const currentMonthStart = startOfMonth(now); + const currentMonthEnd = endOfMonth(now); + const prevMonthStart = startOfMonth(subMonths(now, 1)); + const prevMonthEnd = endOfMonth(subMonths(now, 1)); + + const totalBalance = accounts.reduce( + (acc, curr) => acc + parseFloat(curr.balance), + 0, + ); + let totalIncome = 0; + let totalExpense = 0; + let currentMonthIncome = 0; + let currentMonthExpense = 0; + let prevMonthIncome = 0; + let prevMonthExpense = 0; + let earliestDate: Date | null = null; + let latestDate: Date | null = null; + let earliestIncomeDate: Date | null = null; + let earliestExpenseDate: Date | null = null; + + for (const tx of transactions) { + const amount = Math.abs(parseFloat(tx.amount)); + const txDate = new Date(tx.date); + + if (!earliestDate || txDate < earliestDate) earliestDate = txDate; + if (!latestDate || txDate > latestDate) latestDate = txDate; + + if (tx.type === "CREDIT") { + totalIncome += amount; + if (!earliestIncomeDate || txDate < earliestIncomeDate) + earliestIncomeDate = txDate; + if (txDate >= currentMonthStart && txDate <= currentMonthEnd) + currentMonthIncome += amount; + if (txDate >= prevMonthStart && txDate <= prevMonthEnd) + prevMonthIncome += amount; + } else if (tx.type === "DEBIT") { + totalExpense += amount; + if (!earliestExpenseDate || txDate < earliestExpenseDate) + earliestExpenseDate = txDate; + if (txDate >= currentMonthStart && txDate <= currentMonthEnd) + currentMonthExpense += amount; + if (txDate >= prevMonthStart && txDate <= prevMonthEnd) + prevMonthExpense += amount; + } + } + + const formatDateRange = (start: Date | null, end: Date | null) => { + if (!start) return "No data yet"; + const endDate = end ?? now; + return `${format(start, "d MMM")} - ${format(endDate, "d MMM yyyy")}`; + }; + + const calcChange = (current: number, previous: number): number | null => { + if (previous === 0) return current > 0 ? 100 : null; + return Math.round(((current - previous) / previous) * 100); + }; + + return { + balance: { + title: "Total Balance", + dateRange: formatDateRange(earliestDate, latestDate), + value: totalBalance.toString(), + changePercent: calcChange( + totalBalance, + totalBalance - (currentMonthIncome - currentMonthExpense), + ), + changeLabel: "Last Month", + }, + income: { + title: "Total Income", + dateRange: formatDateRange(earliestIncomeDate, latestDate), + value: totalIncome.toString(), + changePercent: calcChange(currentMonthIncome, prevMonthIncome), + changeLabel: "Last Month", + }, + spending: { + title: "Total Spending", + dateRange: formatDateRange(earliestExpenseDate, latestDate), + value: totalExpense.toString(), + changePercent: calcChange(currentMonthExpense, prevMonthExpense), + changeLabel: "Last Month", + }, + }; + }, [accounts, transactions]); +} diff --git a/src/app/(features)/overview/_hooks/use-pie-chart-data.ts b/src/app/(features)/overview/_hooks/use-pie-chart-data.ts new file mode 100644 index 0000000..0b0ae00 --- /dev/null +++ b/src/app/(features)/overview/_hooks/use-pie-chart-data.ts @@ -0,0 +1,50 @@ +import { useMemo } from "react"; +import type { Transaction } from "@/types/transaction"; +import type { ChartConfig } from "@ui/chart"; + +interface PieChartItem { + name: string; + value: number; + fill: string; +} + +interface CategoryLike { + name: string; + color: string | null; +} + +export function usePieChartData( + transactions: Transaction[], + categoryMap: Map, + categories: CategoryLike[], +): { pieChartData: PieChartItem[]; pieChartConfig: ChartConfig } { + const pieChartData = useMemo(() => { + const categoryTotals: Record = {}; + + for (const tx of transactions) { + if (tx.type === "DEBIT") { + const amount = Math.abs(parseFloat(tx.amount)); + const catName = tx.categoryId + ? (categoryMap.get(tx.categoryId) ?? "Other") + : "Uncategorized"; + categoryTotals[catName] = (categoryTotals[catName] ?? 0) + amount; + } + } + + return Object.entries(categoryTotals).map(([name, value], index) => { + const category = categories.find((c) => c.name === name); + const fill = category?.color ?? `var(--chart-${(index % 5) + 1})`; + return { name, value, fill }; + }); + }, [transactions, categoryMap, categories]); + + const pieChartConfig = useMemo(() => { + const config: ChartConfig = {}; + for (const item of pieChartData) { + config[item.name] = { label: item.name, color: item.fill }; + } + return config; + }, [pieChartData]); + + return { pieChartData, pieChartConfig }; +} diff --git a/src/app/(features)/overview/loading.tsx b/src/app/(features)/overview/loading.tsx index 57cb2cb..64039eb 100644 --- a/src/app/(features)/overview/loading.tsx +++ b/src/app/(features)/overview/loading.tsx @@ -1,6 +1,6 @@ -import { StatsSkeleton } from "@/components/skeletons/stats-skeleton"; -import { ChartSkeleton } from "@/components/skeletons/chart-skeleton"; -import { TableSkeleton } from "@/components/skeletons/table-skeleton"; +import { StatsSkeleton } from "@skeletons/stats-skeleton"; +import { ChartSkeleton } from "@skeletons/chart-skeleton"; +import { TableSkeleton } from "@skeletons/table-skeleton"; export default function OverviewLoading() { return ( diff --git a/src/app/(features)/profile/_client.tsx b/src/app/(features)/profile/_client.tsx index dfb16b7..1133dd4 100644 --- a/src/app/(features)/profile/_client.tsx +++ b/src/app/(features)/profile/_client.tsx @@ -1,9 +1,9 @@ "use client"; -import React, { Suspense, useState } from "react"; +import React, { Suspense, useCallback, useState } from "react"; import dynamic from "next/dynamic"; import { cn } from "@/lib/utils"; -import { Skeleton } from "@/components/ui/skeleton"; +import { Skeleton } from "@ui/skeleton"; import { UserPen, Bell, ShieldCheck, CreditCard } from "lucide-react"; const sectionFallback = ; @@ -37,6 +37,10 @@ type SectionId = (typeof sections)[number]["id"]; export default function ProfilePageClient() { const [activeSection, setActiveSection] = useState("edit-profile"); + const handleSectionChange = useCallback((id: SectionId) => { + setActiveSection(id); + }, []); + return (
{/* Sidebar Navigation */} @@ -48,7 +52,7 @@ export default function ProfilePageClient() { return (
- - - - Need help? - - - - - - - - - - - - - - - ); -} diff --git a/src/components/layout/dashboard-header.tsx b/src/components/common/layout/dashboard-header.tsx similarity index 100% rename from src/components/layout/dashboard-header.tsx rename to src/components/common/layout/dashboard-header.tsx diff --git a/src/components/common/footer.tsx b/src/components/common/layout/footer.tsx similarity index 98% rename from src/components/common/footer.tsx rename to src/components/common/layout/footer.tsx index eb6d524..3d77b2a 100644 --- a/src/components/common/footer.tsx +++ b/src/components/common/layout/footer.tsx @@ -1,4 +1,4 @@ -import { Logo, ThemeSwitcher } from "@component/common"; +import { Logo, ThemeSwitcher } from "@common/index"; import Link from "next/link"; import { NAV_FOOTER_LINKS, SOCIAL_LINKS } from "@/content/nav-links"; import { Github } from "lucide-react"; diff --git a/src/components/common/header.tsx b/src/components/common/layout/header.tsx similarity index 94% rename from src/components/common/header.tsx rename to src/components/common/layout/header.tsx index e73bf4a..fd9b8a9 100644 --- a/src/components/common/header.tsx +++ b/src/components/common/layout/header.tsx @@ -10,12 +10,12 @@ import { MobileNavHeader, MobileNavToggle, MobileNavMenu, -} from "@/components/ui/resizable-navbar"; -import { useState } from "react"; +} from "@ui/resizable-navbar"; +import React, { useState } from "react"; import { NAV_HEADER_LINKS } from "@/content/nav-links"; import Link from "next/link"; -export function Header() { +function HeaderInner() { const navItems = NAV_HEADER_LINKS; const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); @@ -87,3 +87,5 @@ export function Header() { ); } + +export const Header = React.memo(HeaderInner); diff --git a/src/components/layout/navbar.tsx b/src/components/common/layout/navbar.tsx similarity index 92% rename from src/components/layout/navbar.tsx rename to src/components/common/layout/navbar.tsx index 40804dc..5aa0958 100644 --- a/src/components/layout/navbar.tsx +++ b/src/components/common/layout/navbar.tsx @@ -1,23 +1,19 @@ "use client"; import Link from "next/link"; -import { Logo } from "@/components/common/logo"; -import NotificationMenu from "@/components/common/notification-menu"; -import UserMenu from "@/components/common/user-menu"; -import ThemeSwitcherButton from "@/components/common/theme-switcher-button"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; +import { Logo } from "@common/branding/logo"; +import NotificationMenu from "@common/layout/notification-menu"; +import UserMenu from "@common/layout/user-menu"; +import ThemeSwitcherButton from "@common/theme/theme-switcher-button"; +import { Button } from "@ui/button"; +import { Input } from "@ui/input"; import { NavigationMenu, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, -} from "@/components/ui/navigation-menu"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; +} from "@ui/navigation-menu"; +import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover"; import { CommandDialog, CommandEmpty, @@ -26,7 +22,7 @@ import { CommandItem, CommandList, CommandSeparator, -} from "@/components/ui/command"; +} from "@ui/command"; import { Search, Menu, diff --git a/src/components/common/notification-menu.tsx b/src/components/common/layout/notification-menu.tsx similarity index 86% rename from src/components/common/notification-menu.tsx rename to src/components/common/layout/notification-menu.tsx index 63ab58f..514f0d1 100644 --- a/src/components/common/notification-menu.tsx +++ b/src/components/common/layout/notification-menu.tsx @@ -1,16 +1,13 @@ "use client"; +import React, { useCallback } from "react"; import { BellIcon } from "lucide-react"; import { formatDistanceToNow } from "date-fns"; import { api } from "@/trpc/react"; -import { invalidateNotifications } from "@/lib/trpc/invalidation"; +import { invalidateNotifications } from "@/trpc/invalidation"; -import { Button } from "@/components/ui/button"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; +import { Button } from "@ui/button"; +import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover"; function Dot({ className }: { className?: string }) { return ( @@ -28,7 +25,7 @@ function Dot({ className }: { className?: string }) { ); } -export default function NotificationMenu() { +function NotificationMenuInner() { const utils = api.useUtils(); const { data: notifications = [] } = api.notification.getLatest.useQuery({ limit: 10, @@ -43,6 +40,17 @@ export default function NotificationMenu() { onSuccess: () => void invalidateNotifications(utils), }); + const handleMarkAllAsRead = useCallback(() => { + markAllAsRead.mutate(); + }, [markAllAsRead]); + + const handleMarkAsRead = useCallback( + (id: string) => { + markAsRead.mutate({ id }); + }, + [markAsRead], + ); + return ( @@ -70,7 +78,7 @@ export default function NotificationMenu() { {unreadCount > 0 && (
); } + +export default React.memo(ThemeSwitcher); diff --git a/src/components/common/transactions-table.tsx b/src/components/common/transactions-table.tsx index f24a326..b5b93fc 100644 --- a/src/components/common/transactions-table.tsx +++ b/src/components/common/transactions-table.tsx @@ -20,7 +20,7 @@ import { TableHead, TableHeader, TableRow, -} from "@/components/ui/table"; +} from "@ui/table"; import { DropdownMenu, DropdownMenuCheckboxItem, @@ -29,19 +29,19 @@ import { DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Checkbox } from "@/components/ui/checkbox"; +} from "@ui/dropdown-menu"; +import { Button } from "@ui/button"; +import { Input } from "@ui/input"; +import { Checkbox } from "@ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select"; -import { Badge } from "@/components/ui/badge"; -import { Skeleton } from "@/components/ui/skeleton"; +} from "@ui/select"; +import { Badge } from "@ui/badge"; +import { Skeleton } from "@ui/skeleton"; import { MoreHorizontal, Search, @@ -67,7 +67,7 @@ import { import { cn } from "@/lib/utils"; import { useDebounce } from "@/hooks/use-debounce"; import { useCategories } from "@/hooks/use-categories"; -import { DeleteDialog } from "@/components/common/delete-dialog"; +import { DeleteDialog } from "@common/delete-dialog"; import { useFormatter } from "@/hooks/use-formatter"; import type { Transaction } from "@/types/transaction"; diff --git a/src/components/common/waitlist-section.tsx b/src/components/common/waitlist-section.tsx index bc81b72..6c0f77f 100644 --- a/src/components/common/waitlist-section.tsx +++ b/src/components/common/waitlist-section.tsx @@ -1,9 +1,9 @@ "use client"; import React, { useState } from "react"; -import { Avatars } from "@component/common"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; +import { Avatars } from "@common/index"; +import { Button } from "@ui/button"; +import { Input } from "@ui/input"; import { toast } from "sonner"; const WaitlistSection = () => { diff --git a/src/components/forms/accounts/account-form.tsx b/src/components/forms/accounts/account-form.tsx index 0e2068e..93ceb4c 100644 --- a/src/components/forms/accounts/account-form.tsx +++ b/src/components/forms/accounts/account-form.tsx @@ -13,31 +13,31 @@ import { FormControl, FormMessage, FormDescription, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import { Switch } from "@/components/ui/switch"; +} from "@ui/form"; +import { Input } from "@ui/input"; +import { Button } from "@ui/button"; +import { Switch } from "@ui/switch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select"; +} from "@ui/select"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, -} from "@/components/ui/dialog"; +} from "@ui/dialog"; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, -} from "@/components/ui/sheet"; +} from "@ui/sheet"; import { createAccountSchema, type CreateAccountInput, @@ -49,8 +49,8 @@ import { toast } from "sonner"; import { useIsMobile } from "@/hooks/use-mobile"; // Import Custom Pickers -import { IconPicker } from "../../common/icon-picker"; -import { ColorPicker } from "../../common/color-picker"; +import { IconPicker } from "@common/pickers/icon-picker"; +import { ColorPicker } from "@common/pickers/color-picker"; import { createLogger } from "@/lib/logging"; const logger = createLogger("account-form"); diff --git a/src/components/forms/auth/login-form.tsx b/src/components/forms/auth/login-form.tsx index cf00913..38f32fa 100644 --- a/src/components/forms/auth/login-form.tsx +++ b/src/components/forms/auth/login-form.tsx @@ -2,22 +2,21 @@ import React from "react"; import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; +import { Button } from "@ui/button"; import { Field, FieldDescription, FieldGroup, FieldLabel, FieldSeparator, -} from "@/components/ui/field"; -import { Input } from "@/components/ui/input"; +} from "@ui/field"; +import { Input } from "@ui/input"; import { useState, useEffect } from "react"; import { Eye, EyeOff } from "lucide-react"; import { useAuth } from "@/hooks/use-auth"; import { toast } from "sonner"; import { useRouter } from "next/navigation"; import Link from "next/link"; -import { useUserStore } from "@/store/userStore"; import { loginSchema } from "@/validation/auth"; export function LoginForm({ @@ -44,8 +43,7 @@ export function LoginForm({ const [loading, setLoading] = useState(false); const [rememberMe, setRememberMe] = useState(true); - const { signIn, requestPasswordReset, refetch: authRefetch } = useAuth(); - const setUser = useUserStore((s) => s.setUser); + const { signIn, requestPasswordReset } = useAuth(); const handleForgotPassword = async () => { if (!email) { @@ -79,25 +77,13 @@ export function LoginForm({ setLoading(true); const loadingToastId = toast.loading("Logging in..."); try { - const returnedUser = await signIn({ + await signIn({ email, password, rememberMe, callbackURL: "/overview", }); - if (returnedUser) { - setUser(returnedUser); - } else { - try { - await authRefetch(); - } catch { - // Failed to refetch auth state after login - } - const persisted = useUserStore.getState().user; - if (persisted) setUser(persisted); - } - toast.success("Logged in successfully"); router.push("/overview"); } catch (err) { diff --git a/src/components/forms/auth/signup-form.tsx b/src/components/forms/auth/signup-form.tsx index 9c17f6f..e547a8a 100644 --- a/src/components/forms/auth/signup-form.tsx +++ b/src/components/forms/auth/signup-form.tsx @@ -2,7 +2,7 @@ import React from "react"; import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; +import { Button } from "@ui/button"; import { Eye, EyeOff } from "lucide-react"; import { Field, @@ -10,13 +10,13 @@ import { FieldGroup, FieldLabel, FieldSeparator, -} from "@/components/ui/field"; -import { Input } from "@/components/ui/input"; +} from "@ui/field"; +import { Input } from "@ui/input"; import { useState, useEffect } from "react"; import { useAuth } from "@/hooks/use-auth"; import { toast } from "sonner"; import { useRouter } from "next/navigation"; -import { getAvatarUrl } from "@/lib/shared/avatar"; +import { getAvatarUrl } from "@shared/avatar"; import { signupSchema } from "@/validation/auth"; export function SignupForm({ diff --git a/src/components/forms/categories/category-form.tsx b/src/components/forms/categories/category-form.tsx index 507e735..e786e9e 100644 --- a/src/components/forms/categories/category-form.tsx +++ b/src/components/forms/categories/category-form.tsx @@ -12,32 +12,32 @@ import { FormLabel, FormControl, FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; +} from "@ui/form"; +import { Input } from "@ui/input"; +import { Button } from "@ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select"; +} from "@ui/select"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, -} from "@/components/ui/dialog"; +} from "@ui/dialog"; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, -} from "@/components/ui/sheet"; -import { Separator } from "@/components/ui/separator"; -import { ScrollArea } from "@/components/ui/scroll-area"; +} from "@ui/sheet"; +import { Separator } from "@ui/separator"; +import { ScrollArea } from "@ui/scroll-area"; import { Loader2, Palette, @@ -60,8 +60,8 @@ import { import { useCategories } from "@/hooks/use-categories"; // Custom Pickers -import { IconPicker } from "@/components/common/icon-picker"; -import { ColorPicker } from "@/components/common/color-picker"; +import { IconPicker } from "@common/pickers/icon-picker"; +import { ColorPicker } from "@common/pickers/color-picker"; import { createLogger } from "@/lib/logging"; const logger = createLogger("category-form"); diff --git a/src/components/forms/categories/subcategory-form.tsx b/src/components/forms/categories/subcategory-form.tsx index 8362d76..15f44e7 100644 --- a/src/components/forms/categories/subcategory-form.tsx +++ b/src/components/forms/categories/subcategory-form.tsx @@ -12,32 +12,32 @@ import { FormLabel, FormControl, FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; +} from "@ui/form"; +import { Input } from "@ui/input"; +import { Button } from "@ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select"; +} from "@ui/select"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, -} from "@/components/ui/dialog"; +} from "@ui/dialog"; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, -} from "@/components/ui/sheet"; -import { Separator } from "@/components/ui/separator"; -import { ScrollArea } from "@/components/ui/scroll-area"; +} from "@ui/sheet"; +import { Separator } from "@ui/separator"; +import { ScrollArea } from "@ui/scroll-area"; import { useCategories } from "@/hooks/use-categories"; import { createSubcategorySchema, @@ -47,8 +47,8 @@ import { import { Loader2, LayoutGrid, Palette, ArrowRight } from "lucide-react"; import { toast } from "sonner"; import { useIsMobile } from "@/hooks/use-mobile"; -import { IconPicker } from "@/components/common/icon-picker"; -import { ColorPicker } from "@/components/common/color-picker"; +import { IconPicker } from "@common/pickers/icon-picker"; +import { ColorPicker } from "@common/pickers/color-picker"; import { createLogger } from "@/lib/logging"; const logger = createLogger("subcategory-form"); diff --git a/src/components/forms/transaction/steps/basic-info-step.tsx b/src/components/forms/transaction/steps/basic-info-step.tsx index 3e7edc3..919184e 100644 --- a/src/components/forms/transaction/steps/basic-info-step.tsx +++ b/src/components/forms/transaction/steps/basic-info-step.tsx @@ -8,22 +8,18 @@ import { FormLabel, FormControl, FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; +} from "@ui/form"; +import { Input } from "@ui/input"; +import { Button } from "@ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select"; -import { - Popover, - PopoverTrigger, - PopoverContent, -} from "@/components/ui/popover"; -import { Calendar } from "@/components/ui/calendar"; +} from "@ui/select"; +import { Popover, PopoverTrigger, PopoverContent } from "@ui/popover"; +import { Calendar } from "@ui/calendar"; import { CalendarIcon, TrendingUp, diff --git a/src/components/forms/transaction/steps/category-step.tsx b/src/components/forms/transaction/steps/category-step.tsx index c6df183..720b8db 100644 --- a/src/components/forms/transaction/steps/category-step.tsx +++ b/src/components/forms/transaction/steps/category-step.tsx @@ -8,15 +8,15 @@ import { FormLabel, FormControl, FormMessage, -} from "@/components/ui/form"; -import { Button } from "@/components/ui/button"; +} from "@ui/form"; +import { Button } from "@ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select"; +} from "@ui/select"; import { DropdownMenu, DropdownMenuContent, @@ -27,8 +27,8 @@ import { DropdownMenuSubContent, DropdownMenuLabel, DropdownMenuSeparator, -} from "@/components/ui/dropdown-menu"; -import { ScrollArea } from "@/components/ui/scroll-area"; +} from "@ui/dropdown-menu"; +import { ScrollArea } from "@ui/scroll-area"; import { Tag, ChevronDown, diff --git a/src/components/forms/transaction/steps/receipt-step.tsx b/src/components/forms/transaction/steps/receipt-step.tsx index a6a0429..9f1f3da 100644 --- a/src/components/forms/transaction/steps/receipt-step.tsx +++ b/src/components/forms/transaction/steps/receipt-step.tsx @@ -3,7 +3,7 @@ import React, { useState, useCallback } from "react"; import type { UseFormReturn } from "react-hook-form"; -import { Button } from "@/components/ui/button"; +import { Button } from "@ui/button"; import { Loader2, Zap, Check } from "lucide-react"; import { toast } from "sonner"; import { api } from "@/trpc/react"; diff --git a/src/components/forms/transaction/steps/recurring-step.tsx b/src/components/forms/transaction/steps/recurring-step.tsx index 2cd6b32..5761bd2 100644 --- a/src/components/forms/transaction/steps/recurring-step.tsx +++ b/src/components/forms/transaction/steps/recurring-step.tsx @@ -8,23 +8,19 @@ import { FormLabel, FormControl, FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; +} from "@ui/form"; +import { Input } from "@ui/input"; +import { Button } from "@ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select"; -import { - Popover, - PopoverTrigger, - PopoverContent, -} from "@/components/ui/popover"; -import { Calendar } from "@/components/ui/calendar"; -import { Switch } from "@/components/ui/switch"; +} from "@ui/select"; +import { Popover, PopoverTrigger, PopoverContent } from "@ui/popover"; +import { Calendar } from "@ui/calendar"; +import { Switch } from "@ui/switch"; import { CalendarIcon } from "lucide-react"; import { cn } from "@/lib/utils"; import type { CreateTransactionInput } from "@/validation/transaction"; diff --git a/src/components/forms/transaction/transaction-form.tsx b/src/components/forms/transaction/transaction-form.tsx index a7f1bd2..d7c5efe 100644 --- a/src/components/forms/transaction/transaction-form.tsx +++ b/src/components/forms/transaction/transaction-form.tsx @@ -11,9 +11,9 @@ import { FormLabel, FormControl, FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; +} from "@ui/form"; +import { Input } from "@ui/input"; +import { Button } from "@ui/button"; import { Sheet, SheetContent, @@ -21,7 +21,7 @@ import { SheetTitle, SheetDescription, SheetFooter, -} from "@/components/ui/sheet"; +} from "@ui/sheet"; import { Loader2 } from "lucide-react"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; diff --git a/src/components/pages/(protected)/accounts/account-card.tsx b/src/components/pages/(protected)/accounts/account-card.tsx index 2c204c1..be48580 100644 --- a/src/components/pages/(protected)/accounts/account-card.tsx +++ b/src/components/pages/(protected)/accounts/account-card.tsx @@ -1,16 +1,16 @@ import React from "react"; import { Wallet, MoreHorizontal, Pencil, Trash2 } from "lucide-react"; -import { Button } from "@/components/ui/button"; +import { Button } from "@ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; +} from "@ui/dropdown-menu"; import { cn } from "@/lib/utils"; -import { ICON_MAP } from "@/components/common/icon-picker"; +import { ICON_MAP } from "@/constants/icons"; import { format } from "date-fns"; import type { BankAccount as Account } from "@/types/account"; diff --git a/src/components/pages/(protected)/budget/budget-card.tsx b/src/components/pages/(protected)/budget/budget-card.tsx index b9c6504..7b6210d 100644 --- a/src/components/pages/(protected)/budget/budget-card.tsx +++ b/src/components/pages/(protected)/budget/budget-card.tsx @@ -1,8 +1,8 @@ "use client"; import React from "react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "@ui/card"; +import { Badge } from "@ui/badge"; import { cn } from "@/lib/utils"; import { AlertTriangle, CheckCircle2 } from "lucide-react"; import { useFormatter } from "@/hooks/use-formatter"; diff --git a/src/components/pages/(protected)/budget/budget-overview.tsx b/src/components/pages/(protected)/budget/budget-overview.tsx index d1b4002..528f6c5 100644 --- a/src/components/pages/(protected)/budget/budget-overview.tsx +++ b/src/components/pages/(protected)/budget/budget-overview.tsx @@ -1,10 +1,10 @@ "use client"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Progress } from "@/components/ui/progress"; -import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "@ui/card"; +import { Progress } from "@ui/progress"; +import { Badge } from "@ui/badge"; import { AlertTriangle } from "lucide-react"; -import { toNum } from "@/lib/shared/decimal"; +import { toNum } from "@shared/decimal"; // Define a minimal type for the budget prop based on what we know the router returns type Budget = { diff --git a/src/components/pages/(protected)/budget/budget-quick-add.tsx b/src/components/pages/(protected)/budget/budget-quick-add.tsx index 0902bac..8703222 100644 --- a/src/components/pages/(protected)/budget/budget-quick-add.tsx +++ b/src/components/pages/(protected)/budget/budget-quick-add.tsx @@ -4,13 +4,13 @@ import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; import { api } from "@/trpc/react"; -import { invalidateBudgets } from "@/lib/trpc/invalidation"; +import { invalidateBudgets } from "@/trpc/invalidation"; import { toast } from "sonner"; import { Loader2, Plus, X } from "lucide-react"; import { useState } from "react"; import { BudgetPeriod } from "@prisma/client"; -import { Button } from "@/components/ui/button"; +import { Button } from "@ui/button"; import { Form, FormControl, @@ -18,16 +18,16 @@ import { FormItem, FormLabel, FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; +} from "@ui/form"; +import { Input } from "@ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select"; -import { Card, CardContent } from "@/components/ui/card"; +} from "@ui/select"; +import { Card, CardContent } from "@ui/card"; const createBudgetSchema = z.object({ categoryId: z.string().min(1, "Category is required"), diff --git a/src/components/pages/(protected)/budget/budget-radar.tsx b/src/components/pages/(protected)/budget/budget-radar.tsx deleted file mode 100644 index 3c6d6d3..0000000 --- a/src/components/pages/(protected)/budget/budget-radar.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { toNum } from "@/lib/shared/decimal"; -import { PolarAngleAxis, PolarGrid, Radar, RadarChart } from "recharts"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - ChartContainer, - ChartTooltip, - ChartTooltipContent, -} from "@/components/ui/chart"; -import type { ChartConfig as ChartConfigType } from "@/components/ui/chart"; - -type Budget = { - id: string; - amount: { toNumber: () => number } | number | string; - spentAmount: { toNumber: () => number } | number | string; - category: { - name: string; - }; -}; - -const chartConfig = { - budget: { - label: "Budget", - color: "hsl(var(--chart-1))", - }, - spent: { - label: "Spent", - color: "hsl(var(--chart-2))", - }, -} satisfies ChartConfigType; - -export default function BudgetRadar({ budgets }: { budgets: Budget[] }) { - // Transform data for Radar Chart - // We want to show top categories - const chartData = budgets.slice(0, 6).map((b) => ({ - category: b.category.name, - budget: toNum(b.amount), - spent: toNum(b.spentAmount), - })); - - if (chartData.length === 0) return null; - - return ( - - - Spending vs Budget - Comparison across your top categories - - - - - } /> - - - - - - - - - ); -} diff --git a/src/components/pages/(protected)/budget/create-budget-dialog.tsx b/src/components/pages/(protected)/budget/create-budget-dialog.tsx index 2057fa4..62deaa0 100644 --- a/src/components/pages/(protected)/budget/create-budget-dialog.tsx +++ b/src/components/pages/(protected)/budget/create-budget-dialog.tsx @@ -4,13 +4,13 @@ import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; import { api } from "@/trpc/react"; -import { invalidateBudgets } from "@/lib/trpc/invalidation"; +import { invalidateBudgets } from "@/trpc/invalidation"; import { toast } from "sonner"; import { Loader2, Plus } from "lucide-react"; import { useState } from "react"; import { BudgetPeriod } from "@prisma/client"; -import { Button } from "@/components/ui/button"; +import { Button } from "@ui/button"; import { Dialog, DialogContent, @@ -19,7 +19,7 @@ import { DialogHeader, DialogTitle, DialogTrigger, -} from "@/components/ui/dialog"; +} from "@ui/dialog"; import { Form, FormControl, @@ -27,15 +27,15 @@ import { FormItem, FormLabel, FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; +} from "@ui/form"; +import { Input } from "@ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select"; +} from "@ui/select"; // Zod schema for budget creation const createBudgetSchema = z.object({ diff --git a/src/components/pages/(protected)/onboarding/step-forms.tsx b/src/components/pages/(protected)/onboarding/step-forms.tsx index ce2b4de..5090aa4 100644 --- a/src/components/pages/(protected)/onboarding/step-forms.tsx +++ b/src/components/pages/(protected)/onboarding/step-forms.tsx @@ -14,18 +14,18 @@ import { DollarSign, Languages, } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; -import { Input } from "@/components/ui/input"; +import { Button } from "@ui/button"; +import { Label } from "@ui/label"; +import { Switch } from "@ui/switch"; +import { Input } from "@ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +} from "@ui/select"; +import { Avatar, AvatarFallback, AvatarImage } from "@ui/avatar"; import { Currency, Language, diff --git a/src/components/pages/(protected)/overview/recent-transactions.tsx b/src/components/pages/(protected)/overview/recent-transactions.tsx index de0c56c..5fd2f21 100644 --- a/src/components/pages/(protected)/overview/recent-transactions.tsx +++ b/src/components/pages/(protected)/overview/recent-transactions.tsx @@ -3,9 +3,9 @@ import React from "react"; import Link from "next/link"; import { ArrowRight } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { TransactionsTable } from "@/components/common/transactions-table"; +import { Button } from "@ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@ui/card"; +import { TransactionsTable } from "@common/transactions-table"; import type { Transaction } from "@/types/transaction"; interface RecentTransactionsProps { diff --git a/src/components/pages/(protected)/overview/spending-by-category-card.tsx b/src/components/pages/(protected)/overview/spending-by-category-card.tsx new file mode 100644 index 0000000..daf75f8 --- /dev/null +++ b/src/components/pages/(protected)/overview/spending-by-category-card.tsx @@ -0,0 +1,51 @@ +"use client"; + +import React, { Suspense } from "react"; +import dynamic from "next/dynamic"; +import { Card, CardContent, CardHeader, CardTitle } from "@ui/card"; +import { Skeleton } from "@ui/skeleton"; +import type { ChartConfig } from "@ui/chart"; + +const PieChart = dynamic( + () => + import("@/components/charts/pie-chart").then((m) => ({ + default: m.PieChart, + })), + { loading: () => }, +); + +interface SpendingByCategoryCardProps { + pieChartData: Array<{ name: string; value: number; fill: string }>; + pieChartConfig: ChartConfig; + formatAmount: (value: number) => string; +} + +function SpendingByCategoryCardInner({ + pieChartData, + pieChartConfig, + formatAmount, +}: SpendingByCategoryCardProps) { + return ( + + + Spending by Category + + + } + > + formatAmount(val)} + /> + + + + ); +} + +export const SpendingByCategoryCard = React.memo(SpendingByCategoryCardInner); diff --git a/src/components/pages/(protected)/overview/spending-overview-card.tsx b/src/components/pages/(protected)/overview/spending-overview-card.tsx new file mode 100644 index 0000000..c604e5d --- /dev/null +++ b/src/components/pages/(protected)/overview/spending-overview-card.tsx @@ -0,0 +1,92 @@ +"use client"; + +import React, { Suspense } from "react"; +import dynamic from "next/dynamic"; +import { CalendarDays } from "lucide-react"; +import { Card, CardContent, CardHeader, CardTitle } from "@ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@ui/select"; +import { Skeleton } from "@ui/skeleton"; +import type { ChartConfig } from "@ui/chart"; + +const BarChart = dynamic( + () => + import("@/components/charts/bar-chart").then((m) => ({ + default: m.BarChart, + })), + { loading: () => }, +); + +export type DateRange = "3" | "6" | "12"; + +const DATE_RANGE_OPTIONS: { value: DateRange; label: string }[] = [ + { value: "3", label: "Last 3 months" }, + { value: "6", label: "Last 6 months" }, + { value: "12", label: "Last 12 months" }, +]; + +const barChartConfig = { + amount: { + label: "Expenses", + color: "var(--chart-1)", + }, +} satisfies ChartConfig; + +interface SpendingOverviewCardProps { + barChartData: Array<{ date: string; amount: number }>; + barRange: DateRange; + onBarRangeChange: (value: DateRange) => void; + formatAmount: (value: number) => string; +} + +function SpendingOverviewCardInner({ + barChartData, + barRange, + onBarRangeChange, + formatAmount, +}: SpendingOverviewCardProps) { + return ( + + + Spending Overview + + + + } + > + formatAmount(val)} + /> + + + + ); +} + +export const SpendingOverviewCard = React.memo(SpendingOverviewCardInner); diff --git a/src/components/pages/(protected)/profile/billing-section.tsx b/src/components/pages/(protected)/profile/billing-section.tsx index 956b289..d8a2abc 100644 --- a/src/components/pages/(protected)/profile/billing-section.tsx +++ b/src/components/pages/(protected)/profile/billing-section.tsx @@ -1,9 +1,9 @@ "use client"; import { useState } from "react"; -import { Card, CardContent } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; +import { Card, CardContent } from "@ui/card"; +import { Button } from "@ui/button"; +import { Badge } from "@ui/badge"; import { Download, Plus, Trash2 } from "lucide-react"; interface PaymentMethod { diff --git a/src/components/pages/(protected)/profile/edit-profile-section.tsx b/src/components/pages/(protected)/profile/edit-profile-section.tsx index dd6df09..cbd878c 100644 --- a/src/components/pages/(protected)/profile/edit-profile-section.tsx +++ b/src/components/pages/(protected)/profile/edit-profile-section.tsx @@ -6,29 +6,27 @@ import type { Country, Timezone, UpdateProfileInput, - User as UserType, } from "@/types/user"; import { useState, useEffect } from "react"; import { useUser } from "@/hooks/use-user"; -import { useUserStore } from "@/store/userStore"; import { toast } from "sonner"; import { GenderOptions, CountryOptions, TimezoneOptions, -} from "@/lib/format-options"; -import { Card, CardContent } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; +} from "@/constants/formatting"; +import { Card, CardContent } from "@ui/card"; +import { Input } from "@ui/input"; +import { Button } from "@ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select"; -import { Label } from "@/components/ui/label"; +} from "@ui/select"; +import { Label } from "@ui/label"; import { CheckCircle, ImagePlus } from "lucide-react"; import Image from "next/image"; @@ -54,17 +52,14 @@ export default function EditProfileSection() { }); useEffect(() => { - const storeUser = useUserStore.getState().user; - const activeUser = user ?? storeUser; - - if (activeUser) { + if (user) { setFormData((prev) => ({ ...prev, - fullName: activeUser.name ?? "", - email: activeUser.email ?? "", - gender: activeUser.gender ?? prev.gender, - country: activeUser.country ?? prev.country, - timezone: activeUser.timezone ?? prev.timezone, + fullName: user.name ?? "", + email: user.email ?? "", + gender: user.gender! ?? prev.gender, + country: user.country! ?? prev.country, + timezone: user.timezone! ?? prev.timezone, })); } }, [user]); @@ -90,8 +85,6 @@ export default function EditProfileSection() { } }; - const setStoreUser = useUserStore((state) => state.setUser); - const handleUpdateProfile = async () => { try { setIsUpdatingProfile(true); @@ -103,48 +96,7 @@ export default function EditProfileSection() { timezone: formData.timezone ?? undefined, }; - const updatedUser = await updateProfile(updateData); - - if (updatedUser) { - const api = updatedUser as unknown as Record; - const idVal = api.id; - const createdVal = api.createdAt; - const updatedVal = api.updatedAt; - const banExpiresVal = api.banExpires; - - const mappedUser: UserType = { - id: - typeof idVal === "string" || typeof idVal === "number" - ? String(idVal) - : "", - name: (api.name as string) ?? "", - email: (api.email as string) ?? "", - emailVerified: Boolean(api.emailVerified ?? false), - image: (api.image as string) ?? "", - gender: (api.gender as Gender) ?? null, - country: (api.country as Country) ?? null, - timezone: (api.timezone as Timezone) ?? null, - banned: Boolean(api.banned ?? false), - banReason: (api.banReason as string) ?? null, - banExpires: - typeof banExpiresVal === "string" || - typeof banExpiresVal === "number" - ? new Date(String(banExpiresVal)) - : null, - role: (api.role as string) ?? "user", - hasCompletedOnboarding: Boolean(api.hasCompletedOnboarding ?? false), - createdAt: - typeof createdVal === "string" || typeof createdVal === "number" - ? new Date(String(createdVal)) - : new Date(), - updatedAt: - typeof updatedVal === "string" || typeof updatedVal === "number" - ? new Date(String(updatedVal)) - : new Date(), - }; - - setStoreUser(mappedUser); - } + await updateProfile(updateData); toast.success("Profile updated successfully"); } catch (error) { diff --git a/src/components/pages/(protected)/profile/notifications-section.tsx b/src/components/pages/(protected)/profile/notifications-section.tsx index 808a767..ce2ed7e 100644 --- a/src/components/pages/(protected)/profile/notifications-section.tsx +++ b/src/components/pages/(protected)/profile/notifications-section.tsx @@ -2,11 +2,11 @@ import { createLogger } from "@/lib/logging"; import { useSettings } from "@/hooks/use-settings"; -import { Card, CardContent } from "@/components/ui/card"; -import { Switch } from "@/components/ui/switch"; -import { Label } from "@/components/ui/label"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Separator } from "@/components/ui/separator"; +import { Card, CardContent } from "@ui/card"; +import { Switch } from "@ui/switch"; +import { Label } from "@ui/label"; +import { Skeleton } from "@ui/skeleton"; +import { Separator } from "@ui/separator"; import type { NotificationPreferences } from "@prisma/client"; const logger = createLogger("profile-notifications"); diff --git a/src/components/pages/(protected)/profile/security-section.tsx b/src/components/pages/(protected)/profile/security-section.tsx index 0c32b8b..15c711c 100644 --- a/src/components/pages/(protected)/profile/security-section.tsx +++ b/src/components/pages/(protected)/profile/security-section.tsx @@ -4,14 +4,14 @@ import type React from "react"; import { useState } from "react"; import { useAuth } from "@/hooks/use-auth"; import useSessions from "@/hooks/use-sessions"; -import { formatTimestamp } from "@/lib/format-options"; +import { formatTimestamp } from "@/constants/formatting"; import { prettyDeviceFromUA } from "@/lib/device-map"; import { toast } from "sonner"; -import { Card, CardContent } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import { Label } from "@/components/ui/label"; -import { Badge } from "@/components/ui/badge"; +import { Card, CardContent } from "@ui/card"; +import { Input } from "@ui/input"; +import { Button } from "@ui/button"; +import { Label } from "@ui/label"; +import { Badge } from "@ui/badge"; import { LogOut, Smartphone } from "lucide-react"; export default function SecuritySection() { diff --git a/src/components/pages/(protected)/settings/account-settings.tsx b/src/components/pages/(protected)/settings/account-settings.tsx index f363db0..125d801 100644 --- a/src/components/pages/(protected)/settings/account-settings.tsx +++ b/src/components/pages/(protected)/settings/account-settings.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { Suspense, useState } from "react"; import dynamic from "next/dynamic"; import { createLogger } from "@/lib/logging"; import { useAccounts } from "@/hooks/use-accounts"; @@ -17,10 +17,10 @@ import { CardDescription, CardHeader, CardTitle, -} from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Switch } from "@/components/ui/switch"; -import { Badge } from "@/components/ui/badge"; +} from "@ui/card"; +import { Button } from "@ui/button"; +import { Switch } from "@ui/switch"; +import { Badge } from "@ui/badge"; import { Plus, ChevronDown, @@ -36,7 +36,7 @@ import { } from "lucide-react"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; -import { DeleteDialog } from "@/components/common/delete-dialog"; +import { DeleteDialog } from "@common/delete-dialog"; import { useFormatter } from "@/hooks/use-formatter"; const ICON_MAP: Record = { @@ -95,7 +95,9 @@ export default function AccountSettings() { return (
- + + + {accounts.length > 0 && (
diff --git a/src/components/pages/(protected)/settings/appearance-settings.tsx b/src/components/pages/(protected)/settings/appearance-settings.tsx index 68ad62c..14ef8ae 100644 --- a/src/components/pages/(protected)/settings/appearance-settings.tsx +++ b/src/components/pages/(protected)/settings/appearance-settings.tsx @@ -8,16 +8,16 @@ import { CardDescription, CardHeader, CardTitle, -} from "@/components/ui/card"; +} from "@ui/card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select"; -import { Label } from "@/components/ui/label"; -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +} from "@ui/select"; +import { Label } from "@ui/label"; +import { RadioGroup, RadioGroupItem } from "@ui/radio-group"; export default function AppearanceSettings() { const { theme: currentTheme, setTheme } = useTheme(); diff --git a/src/components/pages/(protected)/settings/billing-settings.tsx b/src/components/pages/(protected)/settings/billing-settings.tsx index be8897c..464997a 100644 --- a/src/components/pages/(protected)/settings/billing-settings.tsx +++ b/src/components/pages/(protected)/settings/billing-settings.tsx @@ -7,9 +7,9 @@ import { CardDescription, CardHeader, CardTitle, -} from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; +} from "@ui/card"; +import { Button } from "@ui/button"; +import { Badge } from "@ui/badge"; import { Download, Plus, Trash2 } from "lucide-react"; interface PaymentMethod { diff --git a/src/components/pages/(protected)/settings/categories-settings.tsx b/src/components/pages/(protected)/settings/categories-settings.tsx index 5283d7d..f74d537 100644 --- a/src/components/pages/(protected)/settings/categories-settings.tsx +++ b/src/components/pages/(protected)/settings/categories-settings.tsx @@ -2,14 +2,14 @@ import React, { useState, useCallback } from "react"; import { api } from "@/trpc/react"; -import { invalidateCategories } from "@/lib/trpc/invalidation"; +import { invalidateCategories } from "@/trpc/invalidation"; import { useCategories } from "@/hooks/use-categories"; import CategoryForm from "@/components/forms/categories/category-form"; import SubcategoryForm from "@/components/forms/categories/subcategory-form"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@ui/card"; // Tabs/Separator removed (not used) -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Alert, AlertDescription, AlertTitle } from "@ui/alert"; import { Trash2, Edit2, @@ -19,14 +19,14 @@ import { LayoutGrid, List, } from "lucide-react"; -import { ICONS } from "@/components/common/icon-picker"; +import { ICONS } from "@/constants/icons"; // toast removed (not used in this file) import type { Category } from "@/types/category"; import type { CreateCategoryInput, CreateSubcategoryInput, } from "@/validation/category"; -import { DeleteDialog } from "@/components/common/delete-dialog"; +import { DeleteDialog } from "@common/delete-dialog"; type CategoryWithChildren = Category & { children?: CategoryWithChildren[] }; diff --git a/src/components/pages/(protected)/settings/display-settings.tsx b/src/components/pages/(protected)/settings/display-settings.tsx index 11b2355..d76a1e1 100644 --- a/src/components/pages/(protected)/settings/display-settings.tsx +++ b/src/components/pages/(protected)/settings/display-settings.tsx @@ -7,15 +7,15 @@ import { CardDescription, CardHeader, CardTitle, -} from "@/components/ui/card"; +} from "@ui/card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select"; -import { Label } from "@/components/ui/label"; +} from "@ui/select"; +import { Label } from "@ui/label"; import { Hash, LayoutDashboard, @@ -28,10 +28,10 @@ import { Sun, Laptop, } from "lucide-react"; -import { Separator } from "@/components/ui/separator"; -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; -import { Switch } from "@/components/ui/switch"; -import { Skeleton } from "@/components/ui/skeleton"; +import { Separator } from "@ui/separator"; +import { RadioGroup, RadioGroupItem } from "@ui/radio-group"; +import { Switch } from "@ui/switch"; +import { Skeleton } from "@ui/skeleton"; import { DefaultView, CurrencyPosition, diff --git a/src/components/pages/(protected)/settings/notifications-settings.tsx b/src/components/pages/(protected)/settings/notifications-settings.tsx index 29a5360..1d3056e 100644 --- a/src/components/pages/(protected)/settings/notifications-settings.tsx +++ b/src/components/pages/(protected)/settings/notifications-settings.tsx @@ -8,11 +8,11 @@ import { CardDescription, CardHeader, CardTitle, -} from "@/components/ui/card"; -import { Switch } from "@/components/ui/switch"; -import { Label } from "@/components/ui/label"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Separator } from "@/components/ui/separator"; +} from "@ui/card"; +import { Switch } from "@ui/switch"; +import { Label } from "@ui/label"; +import { Skeleton } from "@ui/skeleton"; +import { Separator } from "@ui/separator"; import type { NotificationPreferences } from "@prisma/client"; const logger = createLogger("notifications-settings"); diff --git a/src/components/pages/(protected)/settings/profile-settings.tsx b/src/components/pages/(protected)/settings/profile-settings.tsx index 4ffdbcc..820c855 100644 --- a/src/components/pages/(protected)/settings/profile-settings.tsx +++ b/src/components/pages/(protected)/settings/profile-settings.tsx @@ -6,36 +6,34 @@ import type { Country, Timezone, UpdateProfileInput, - User as UserType, } from "@/types/user"; import { useState, useEffect } from "react"; import { useAuth } from "@/hooks/use-auth"; import { useUser } from "@/hooks/use-user"; -import { useUserStore } from "@/store/userStore"; import { toast } from "sonner"; import { GenderOptions, CountryOptions, TimezoneOptions, -} from "@/lib/format-options"; +} from "@/constants/formatting"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, -} from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; +} from "@ui/card"; +import { Input } from "@ui/input"; +import { Button } from "@ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select"; -import { Label } from "@/components/ui/label"; +} from "@ui/select"; +import { Label } from "@ui/label"; import { Upload, Lock, @@ -75,18 +73,14 @@ export default function ProfileSettings() { }); useEffect(() => { - const storeUser = useUserStore.getState().user; - const activeUser = user ?? storeUser; - - if (activeUser) { + if (user) { setFormData((prev) => ({ ...prev, - fullName: activeUser.name ?? "", - email: activeUser.email ?? "", - // preserve previous value when activeUser.gender is null/undefined - gender: activeUser.gender ?? prev.gender, - country: activeUser.country ?? prev.country, - timezone: activeUser.timezone ?? prev.timezone, + fullName: user.name ?? "", + email: user.email ?? "", + gender: user.gender! ?? prev.gender, + country: user.country! ?? prev.country, + timezone: user.timezone! ?? prev.timezone, })); } }, [user]); @@ -112,8 +106,6 @@ export default function ProfileSettings() { } }; - const setStoreUser = useUserStore((state) => state.setUser); - const handleUpdateProfile = async () => { try { setLoadingState((prev) => ({ ...prev, isUpdatingProfile: true })); @@ -125,51 +117,7 @@ export default function ProfileSettings() { timezone: formData.timezone ?? undefined, }; - // Update profile via tRPC - const updatedUser = await updateProfile(updateData); - - // Update Zustand store with the new user data - if (updatedUser) { - // Map API-shaped user (dates as strings) to the app `User` type (Date objects) - const api = updatedUser as unknown as Record; - const idVal = api.id; - const createdVal = api.createdAt; - const updatedVal = api.updatedAt; - const banExpiresVal = api.banExpires; - - const mappedUser: UserType = { - id: - typeof idVal === "string" || typeof idVal === "number" - ? String(idVal) - : "", - name: (api.name as string) ?? "", - email: (api.email as string) ?? "", - emailVerified: Boolean(api.emailVerified ?? false), - image: (api.image as string) ?? "", - gender: (api.gender as Gender) ?? null, - country: (api.country as Country) ?? null, - timezone: (api.timezone as Timezone) ?? null, - banned: Boolean(api.banned ?? false), - banReason: (api.banReason as string) ?? null, - banExpires: - typeof banExpiresVal === "string" || - typeof banExpiresVal === "number" - ? new Date(String(banExpiresVal)) - : null, - role: (api.role as string) ?? "user", - hasCompletedOnboarding: Boolean(api.hasCompletedOnboarding ?? false), - createdAt: - typeof createdVal === "string" || typeof createdVal === "number" - ? new Date(String(createdVal)) - : new Date(), - updatedAt: - typeof updatedVal === "string" || typeof updatedVal === "number" - ? new Date(String(updatedVal)) - : new Date(), - }; - - setStoreUser(mappedUser); - } + await updateProfile(updateData); toast.success("Profile updated successfully"); } catch (error) { diff --git a/src/components/pages/(protected)/settings/sessions-settings.tsx b/src/components/pages/(protected)/settings/sessions-settings.tsx index ee3ccfe..ba3c299 100644 --- a/src/components/pages/(protected)/settings/sessions-settings.tsx +++ b/src/components/pages/(protected)/settings/sessions-settings.tsx @@ -2,7 +2,7 @@ import { toast } from "sonner"; import useSessions from "@/hooks/use-sessions"; -import { formatTimestamp } from "@/lib/format-options"; +import { formatTimestamp } from "@/constants/formatting"; import { prettyDeviceFromUA } from "@/lib/device-map"; import { Card, @@ -10,9 +10,9 @@ import { CardDescription, CardHeader, CardTitle, -} from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; +} from "@ui/card"; +import { Button } from "@ui/button"; +import { Badge } from "@ui/badge"; import { LogOut, Smartphone } from "lucide-react"; interface ActiveSession { diff --git a/src/components/pages/(protected)/transactions/bulk-import/bulk-import-dialog.tsx b/src/components/pages/(protected)/transactions/bulk-import/bulk-import-dialog.tsx index 416f65c..b66aea3 100644 --- a/src/components/pages/(protected)/transactions/bulk-import/bulk-import-dialog.tsx +++ b/src/components/pages/(protected)/transactions/bulk-import/bulk-import-dialog.tsx @@ -5,7 +5,7 @@ import { DialogDescription, DialogHeader, DialogTitle, -} from "@/components/ui/dialog"; +} from "@ui/dialog"; import { FileUploadStep } from "./steps/file-upload-step"; import { ColumnMappingStep } from "./steps/column-mapping-step"; import { ConfirmImportStep } from "./steps/confirm-import-step"; diff --git a/src/components/pages/(protected)/transactions/bulk-import/steps/column-mapping-step.tsx b/src/components/pages/(protected)/transactions/bulk-import/steps/column-mapping-step.tsx index 2028612..f230f71 100644 --- a/src/components/pages/(protected)/transactions/bulk-import/steps/column-mapping-step.tsx +++ b/src/components/pages/(protected)/transactions/bulk-import/steps/column-mapping-step.tsx @@ -1,17 +1,17 @@ "use client"; import React, { useMemo, useState } from "react"; -import { Button } from "@/components/ui/button"; +import { Button } from "@ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select"; -import { Alert, AlertDescription } from "@/components/ui/alert"; +} from "@ui/select"; +import { Alert, AlertDescription } from "@ui/alert"; import { AlertCircle } from "lucide-react"; -import { validateAndParseTransactions } from "@/lib/shared/file-parser"; +import { validateAndParseTransactions } from "@shared/file-parser"; import type { TransactionField, ColumnMapping, diff --git a/src/components/pages/(protected)/transactions/bulk-import/steps/confirm-import-step.tsx b/src/components/pages/(protected)/transactions/bulk-import/steps/confirm-import-step.tsx index 654511c..caecae7 100644 --- a/src/components/pages/(protected)/transactions/bulk-import/steps/confirm-import-step.tsx +++ b/src/components/pages/(protected)/transactions/bulk-import/steps/confirm-import-step.tsx @@ -1,17 +1,17 @@ "use client"; import React, { useMemo, useState } from "react"; -import { Button } from "@/components/ui/button"; -import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Button } from "@ui/button"; +import { Alert, AlertDescription } from "@ui/alert"; import { AlertTriangle } from "lucide-react"; -import { Separator } from "@/components/ui/separator"; +import { Separator } from "@ui/separator"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select"; +} from "@ui/select"; import { useAccounts } from "@/hooks/use-accounts"; import type { ImportError, ImportTransaction } from "@/types/bulk-import"; diff --git a/src/components/pages/(protected)/transactions/bulk-import/steps/file-upload-step.tsx b/src/components/pages/(protected)/transactions/bulk-import/steps/file-upload-step.tsx index a3a5482..9e1c0d6 100644 --- a/src/components/pages/(protected)/transactions/bulk-import/steps/file-upload-step.tsx +++ b/src/components/pages/(protected)/transactions/bulk-import/steps/file-upload-step.tsx @@ -2,9 +2,10 @@ import React, { useRef, useState } from "react"; import { Upload, AlertCircle } from "lucide-react"; -import { Alert, AlertDescription } from "@/components/ui/alert"; -import { parseCSV } from "@/lib/shared/file-parser"; -import { Skeleton } from "@/components/ui/skeleton"; +import { Alert, AlertDescription } from "@ui/alert"; +import { parseCSV } from "@shared/file-parser"; +import { env } from "@/env"; +import { Skeleton } from "@ui/skeleton"; interface FileUploadStepProps { onNext: (file: File, csvData: Record[]) => void; @@ -37,8 +38,8 @@ export function FileUploadStep({ onNext }: FileUploadStepProps) { } // Validate row count against configured maximum - const maxRows = process.env.NEXT_PUBLIC_GEMINI_MAX_ROWS - ? Number(process.env.NEXT_PUBLIC_GEMINI_MAX_ROWS) + const maxRows = env.NEXT_PUBLIC_GEMINI_MAX_ROWS + ? Number(env.NEXT_PUBLIC_GEMINI_MAX_ROWS) : 50; if (csvData.length > maxRows) { throw new Error( diff --git a/src/components/pages/(protected)/transactions/bulk-import/steps/import-progress-step.tsx b/src/components/pages/(protected)/transactions/bulk-import/steps/import-progress-step.tsx index f1d1feb..043d660 100644 --- a/src/components/pages/(protected)/transactions/bulk-import/steps/import-progress-step.tsx +++ b/src/components/pages/(protected)/transactions/bulk-import/steps/import-progress-step.tsx @@ -1,11 +1,11 @@ "use client"; import React, { useEffect, useState } from "react"; -import { Button } from "@/components/ui/button"; -import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Button } from "@ui/button"; +import { Alert, AlertDescription } from "@ui/alert"; import { CheckCircle2, AlertCircle, Loader2 } from "lucide-react"; -import { Progress } from "@/components/ui/progress"; -import { ScrollArea } from "@/components/ui/scroll-area"; +import { Progress } from "@ui/progress"; +import { ScrollArea } from "@ui/scroll-area"; import { useTransactions } from "@/hooks/use-transactions"; import { toast } from "sonner"; import type { ImportTransaction } from "@/types/bulk-import"; diff --git a/src/components/pages/(protected)/transactions/transactions-analytics.tsx b/src/components/pages/(protected)/transactions/transactions-analytics.tsx deleted file mode 100644 index 50a6fe5..0000000 --- a/src/components/pages/(protected)/transactions/transactions-analytics.tsx +++ /dev/null @@ -1,197 +0,0 @@ -"use client"; - -import { useMemo } from "react"; -import { - Card, - CardContent, - CardHeader, - CardTitle, - CardDescription, - CardFooter, -} from "@/components/ui/card"; -import { AreaChart } from "@/components/charts/area-chart"; -import { type ChartConfig } from "@/components/ui/chart"; -import { - TrendingUp, - TrendingDown, - Wallet, - ArrowUpRight, - ArrowDownRight, -} from "lucide-react"; -import { useFormatter } from "@/hooks/use-formatter"; - -interface TransactionsAnalyticsProps { - chartData: { date: string; income: number; expense: number }[]; - chartConfig: ChartConfig; -} - -export function TransactionsAnalytics({ - chartData, - chartConfig, -}: TransactionsAnalyticsProps) { - const { formatAmount } = useFormatter(); - - // 1. Calculate totals and trends - const analytics = useMemo(() => { - const totalIncome = chartData.reduce((acc, curr) => acc + curr.income, 0); - const totalExpense = chartData.reduce((acc, curr) => acc + curr.expense, 0); - - // Split data in half to calculate a trend if we don't have previous month data - const midPoint = Math.floor(chartData.length / 2); - const firstHalf = chartData.slice(0, midPoint); - const secondHalf = chartData.slice(midPoint); - - const firstHalfIncome = firstHalf.reduce( - (acc, curr) => acc + curr.income, - 0, - ); - const secondHalfIncome = secondHalf.reduce( - (acc, curr) => acc + curr.income, - 0, - ); - const incomeTrend = - firstHalfIncome > 0 - ? ((secondHalfIncome - firstHalfIncome) / firstHalfIncome) * 100 - : 0; - - const firstHalfExpense = firstHalf.reduce( - (acc, curr) => acc + curr.expense, - 0, - ); - const secondHalfExpense = secondHalf.reduce( - (acc, curr) => acc + curr.expense, - 0, - ); - const expenseTrend = - firstHalfExpense > 0 - ? ((secondHalfExpense - firstHalfExpense) / firstHalfExpense) * 100 - : 0; - - const netTrend = (incomeTrend + expenseTrend) / 2; - - return { - totals: { income: totalIncome, expense: totalExpense }, - incomeTrend, - expenseTrend, - netTrend, - netBalance: totalIncome - totalExpense, - }; - }, [chartData]); - - const { totals, incomeTrend, expenseTrend, netTrend, netBalance } = analytics; - - return ( - - {/* Header Section */} - -
- - Financial Overview - - Income vs Expenses over time -
- {/* Optional: Add a simple time range badge or dropdown here */} -
- Last 30 Days -
-
- - - {/* Metric Cards - This fills the empty space with useful data */} -
- {/* Net Balance Block */} -
-
- - Net Balance -
-
{formatAmount(netBalance)}
-

- Total cash flow for this period -

-
- - {/* Income Block */} -
-
- - Total Income -
-
- {formatAmount(totals.income)} -
-
- {incomeTrend >= 0 ? ( - - ) : ( - - )} - - {incomeTrend >= 0 ? "+" : ""} - {incomeTrend.toFixed(1)}% - - - vs previous period - -
-
- - {/* Expense Block */} -
-
- - Total Expenses -
-
- {formatAmount(totals.expense)} -
-
- {expenseTrend >= 0 ? ( - - ) : ( - - )} - - {expenseTrend >= 0 ? "+" : ""} - {expenseTrend.toFixed(1)}% - - - vs previous period - -
-
-
- - {/* The Chart */} -
- -
-
- - -
-
-
- {netTrend >= 0 ? "Income trending up" : "Spending trend shifting"}{" "} - by {Math.abs(netTrend).toFixed(1)}% this period{" "} - {netTrend >= 0 ? ( - - ) : ( - - )} -
-
- Showing total volume for the last {chartData.length} days -
-
-
-
-
- ); -} diff --git a/src/components/pages/(protected)/transactions/transactions-header.tsx b/src/components/pages/(protected)/transactions/transactions-header.tsx index 413c876..f4ec7ca 100644 --- a/src/components/pages/(protected)/transactions/transactions-header.tsx +++ b/src/components/pages/(protected)/transactions/transactions-header.tsx @@ -1,7 +1,7 @@ "use client"; import React from "react"; -import { Button } from "@/components/ui/button"; +import { Button } from "@ui/button"; import { PlusCircle, Upload } from "lucide-react"; interface TransactionsHeaderProps { diff --git a/src/components/pages/(protected)/transactions/transactions-list.tsx b/src/components/pages/(protected)/transactions/transactions-list.tsx index 389c031..77dd07d 100644 --- a/src/components/pages/(protected)/transactions/transactions-list.tsx +++ b/src/components/pages/(protected)/transactions/transactions-list.tsx @@ -1,6 +1,6 @@ "use client"; -import { TransactionsTable } from "@/components/common/transactions-table"; +import { TransactionsTable } from "@common/transactions-table"; import type { Transaction } from "@/types/transaction"; interface TransactionsListProps { diff --git a/src/components/pages/(public)/about/blog-card.tsx b/src/components/pages/(public)/about/blog-card.tsx index 140f2fc..4e3065b 100644 --- a/src/components/pages/(public)/about/blog-card.tsx +++ b/src/components/pages/(public)/about/blog-card.tsx @@ -1,6 +1,6 @@ import { ArrowRightIcon } from "lucide-react"; import Image from "next/image"; -import { Button } from "@/components/ui/button"; +import { Button } from "@ui/button"; import { Card, CardContent, @@ -8,7 +8,7 @@ import { CardDescription, CardHeader, CardFooter, -} from "@/components/ui/card"; +} from "@ui/card"; import { blog as siteBlog } from "@content/site/blog"; import type { BlogCard as ContentBlogCard } from "@/types"; diff --git a/src/components/pages/(public)/about/faq-section.tsx b/src/components/pages/(public)/about/faq-section.tsx index 214e261..07fc28e 100644 --- a/src/components/pages/(public)/about/faq-section.tsx +++ b/src/components/pages/(public)/about/faq-section.tsx @@ -3,7 +3,7 @@ import { AccordionContent, AccordionItem, AccordionTrigger, -} from "@/components/ui/accordion"; +} from "@ui/accordion"; import { faq } from "@content/site/about"; const FAQSection = () => { diff --git a/src/components/pages/(public)/about/hero-section.tsx b/src/components/pages/(public)/about/hero-section.tsx index 3845f78..b879f8a 100644 --- a/src/components/pages/(public)/about/hero-section.tsx +++ b/src/components/pages/(public)/about/hero-section.tsx @@ -1,8 +1,8 @@ "use client"; import Image from "next/image"; import Link from "next/link"; -import { Avatars } from "@component/common"; -import { Button } from "@/components/ui/button"; +import { Avatars } from "@common/index"; +import { Button } from "@ui/button"; import { CirclePlay } from "lucide-react"; import { toast } from "sonner"; import { hero, stats as statsContent } from "@content/site/about"; diff --git a/src/components/pages/(public)/blog/blog-post-card.tsx b/src/components/pages/(public)/blog/blog-post-card.tsx index 64ac621..ac1fae2 100644 --- a/src/components/pages/(public)/blog/blog-post-card.tsx +++ b/src/components/pages/(public)/blog/blog-post-card.tsx @@ -1,9 +1,10 @@ -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import React from "react"; +import { Avatar, AvatarFallback, AvatarImage } from "@ui/avatar"; import Image from "next/image"; import Link from "next/link"; import type { BlogCard } from "@/types/site"; -export function BlogPostCard({ +function BlogPostCardInner({ imageSrc, imageAlt, title, @@ -66,3 +67,5 @@ export function BlogPostCard({ return card; } + +export const BlogPostCard = React.memo(BlogPostCardInner); diff --git a/src/components/pages/(public)/blog/comment-section.tsx b/src/components/pages/(public)/blog/comment-section.tsx index 1010f6e..060fe50 100644 --- a/src/components/pages/(public)/blog/comment-section.tsx +++ b/src/components/pages/(public)/blog/comment-section.tsx @@ -1,8 +1,8 @@ "use client"; import { useState } from "react"; -import { Button } from "@/components/ui/button"; -import { Textarea } from "@/components/ui/textarea"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Button } from "@ui/button"; +import { Textarea } from "@ui/textarea"; +import { Avatar, AvatarFallback, AvatarImage } from "@ui/avatar"; import { Heart, MessageCircle, Send } from "lucide-react"; import type { BlogComment } from "@/types/site"; diff --git a/src/components/pages/(public)/blog/header-section.tsx b/src/components/pages/(public)/blog/header-section.tsx index 7565520..74789e8 100644 --- a/src/components/pages/(public)/blog/header-section.tsx +++ b/src/components/pages/(public)/blog/header-section.tsx @@ -1,7 +1,7 @@ import { Facebook, Twitter, Linkedin } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Badge } from "@/components/ui/badge"; +import { Button } from "@ui/button"; +import { Avatar, AvatarFallback, AvatarImage } from "@ui/avatar"; +import { Badge } from "@ui/badge"; import type { BlogPost } from "@/types/site"; import Link from "next/link"; import { SOCIAL_LINKS } from "@/content/nav-links"; diff --git a/src/components/pages/(public)/features/content-section.tsx b/src/components/pages/(public)/features/content-section.tsx index 285986a..b10b64e 100644 --- a/src/components/pages/(public)/features/content-section.tsx +++ b/src/components/pages/(public)/features/content-section.tsx @@ -1,4 +1,4 @@ -import { Button } from "@/components/ui/button"; +import { Button } from "@ui/button"; import { ChevronRight } from "lucide-react"; import Link from "next/link"; import { featuresContent } from "@content/site/features"; diff --git a/src/components/pages/(public)/features/feature-card.tsx b/src/components/pages/(public)/features/feature-card.tsx index 7fb7f50..be3c2fd 100644 --- a/src/components/pages/(public)/features/feature-card.tsx +++ b/src/components/pages/(public)/features/feature-card.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { Building2, Lightbulb, @@ -48,7 +49,7 @@ const rightFeatures: FeatureItem[] = featuresContent.rightFeatures.map((f) => ({ cornerStyle: f.cornerStyle, })); -const FeatureCard = ({ feature }: { feature: FeatureItem }) => { +const FeatureCardInner = ({ feature }: { feature: FeatureItem }) => { const Icon = feature.icon; return ( @@ -75,6 +76,8 @@ const FeatureCard = ({ feature }: { feature: FeatureItem }) => { ); }; +const FeatureCard = React.memo(FeatureCardInner); + export default function FeatureCards() { return (
diff --git a/src/components/pages/(public)/features/features.tsx b/src/components/pages/(public)/features/features.tsx index 3ad5477..6645173 100644 --- a/src/components/pages/(public)/features/features.tsx +++ b/src/components/pages/(public)/features/features.tsx @@ -1,4 +1,4 @@ -import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Card, CardContent, CardHeader } from "@ui/card"; import { cn } from "@/lib/utils"; import { Calendar, MapIcon } from "lucide-react"; import type { LucideIcon } from "lucide-react"; diff --git a/src/components/pages/(public)/help/faq.tsx b/src/components/pages/(public)/help/faq.tsx index 4bf0beb..83ef636 100644 --- a/src/components/pages/(public)/help/faq.tsx +++ b/src/components/pages/(public)/help/faq.tsx @@ -3,7 +3,7 @@ import { AccordionContent, AccordionItem, AccordionTrigger, -} from "@/components/ui/accordion"; +} from "@ui/accordion"; import helpContent from "@/content/site/help"; diff --git a/src/components/pages/(public)/help/list-item.tsx b/src/components/pages/(public)/help/list-item.tsx index f4793b3..b24b8d5 100644 --- a/src/components/pages/(public)/help/list-item.tsx +++ b/src/components/pages/(public)/help/list-item.tsx @@ -5,7 +5,7 @@ import { CardDescription, CardHeader, CardTitle, -} from "@/components/ui/card"; +} from "@ui/card"; import Link from "next/link"; interface HelpCenterCardProps { diff --git a/src/components/pages/(public)/help/search-bar.tsx b/src/components/pages/(public)/help/search-bar.tsx index 8be03b2..ebbf4f9 100644 --- a/src/components/pages/(public)/help/search-bar.tsx +++ b/src/components/pages/(public)/help/search-bar.tsx @@ -1,4 +1,4 @@ -import { Input } from "@/components/ui/input"; +import { Input } from "@ui/input"; import { SearchIcon } from "lucide-react"; import helpContent from "@/content/site/help"; diff --git a/src/components/pages/(public)/home/feature-section.tsx b/src/components/pages/(public)/home/feature-section.tsx index 54cf407..e52954b 100644 --- a/src/components/pages/(public)/home/feature-section.tsx +++ b/src/components/pages/(public)/home/feature-section.tsx @@ -2,9 +2,9 @@ import type { ComponentType } from "react"; import { ArrowRightIcon } from "lucide-react"; -import { Avatar, AvatarFallback } from "@/components/ui/avatar"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent } from "@/components/ui/card"; +import { Avatar, AvatarFallback } from "@ui/avatar"; +import { Button } from "@ui/button"; +import { Card, CardContent } from "@ui/card"; import { cn } from "@/lib/utils"; diff --git a/src/components/pages/(public)/home/hero-section.tsx b/src/components/pages/(public)/home/hero-section.tsx index 5d550e4..a4b16ce 100644 --- a/src/components/pages/(public)/home/hero-section.tsx +++ b/src/components/pages/(public)/home/hero-section.tsx @@ -1,13 +1,14 @@ "use client"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; +import React from "react"; +import { Badge } from "@ui/badge"; +import { Button } from "@ui/button"; import { ArrowUpRight, CirclePlay } from "lucide-react"; -import { BackgroundPattern } from "@component/common"; +import { BackgroundPattern } from "@common/index"; import Link from "next/link"; import { toast } from "sonner"; import { hero } from "@content/site/home"; -const HeroSection = () => { +const HeroSectionInner = () => { const data = hero; return ( @@ -56,4 +57,5 @@ const HeroSection = () => { ); }; +const HeroSection = React.memo(HeroSectionInner); export default HeroSection; diff --git a/src/components/pages/(public)/home/integrations-section.tsx b/src/components/pages/(public)/home/integrations-section.tsx index 4ee8bad..641825d 100644 --- a/src/components/pages/(public)/home/integrations-section.tsx +++ b/src/components/pages/(public)/home/integrations-section.tsx @@ -6,9 +6,9 @@ import { VSCodium, MediaWiki, } from "public/images/logos"; -import { LogoIcon } from "@/components/common/logo"; +import { LogoIcon } from "@common/branding/logo"; import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; +import { Button } from "@ui/button"; import Link from "next/link"; import type { IntegrationContent } from "@/types/site"; import { integrations as integrationsContent } from "@content/site/home"; diff --git a/src/components/pages/(public)/home/pricing-section.tsx b/src/components/pages/(public)/home/pricing-section.tsx index 73a10dc..705297a 100644 --- a/src/components/pages/(public)/home/pricing-section.tsx +++ b/src/components/pages/(public)/home/pricing-section.tsx @@ -1,13 +1,14 @@ -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Separator } from "@/components/ui/separator"; +import React from "react"; +import { Badge } from "@ui/badge"; +import { Button } from "@ui/button"; +import { Separator } from "@ui/separator"; import { cn } from "@/lib/utils"; import { CircleCheck } from "lucide-react"; import type { Plan } from "@/types/site"; import { pricing as pricingContent } from "@content/site/home"; const plans: Plan[] = pricingContent.plans; -const PricingSection = () => { +const PricingSectionInner = () => { return (

@@ -57,4 +58,5 @@ const PricingSection = () => { ); }; +const PricingSection = React.memo(PricingSectionInner); export default PricingSection; diff --git a/src/components/pages/(public)/home/testimonials-section.tsx b/src/components/pages/(public)/home/testimonials-section.tsx index b3738ef..1b21c32 100644 --- a/src/components/pages/(public)/home/testimonials-section.tsx +++ b/src/components/pages/(public)/home/testimonials-section.tsx @@ -1,5 +1,5 @@ -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Card, CardContent } from "@/components/ui/card"; +import { Avatar, AvatarFallback, AvatarImage } from "@ui/avatar"; +import { Card, CardContent } from "@ui/card"; import { testimonials as rawTestimonials } from "@content/site/home"; import type { Testimonial } from "@/types/site"; diff --git a/src/components/skeletons/accounts-skeleton.tsx b/src/components/skeletons/accounts-skeleton.tsx index 46faa2b..0f6f27b 100644 --- a/src/components/skeletons/accounts-skeleton.tsx +++ b/src/components/skeletons/accounts-skeleton.tsx @@ -1,4 +1,4 @@ -import { Skeleton } from "@/components/ui/skeleton"; +import { Skeleton } from "@ui/skeleton"; export function AccountsSkeleton() { return ( diff --git a/src/components/skeletons/budget-skeleton.tsx b/src/components/skeletons/budget-skeleton.tsx index 0128b2a..ce7020f 100644 --- a/src/components/skeletons/budget-skeleton.tsx +++ b/src/components/skeletons/budget-skeleton.tsx @@ -1,4 +1,4 @@ -import { Skeleton } from "@/components/ui/skeleton"; +import { Skeleton } from "@ui/skeleton"; export function BudgetSkeleton() { return ( diff --git a/src/components/skeletons/chart-skeleton.tsx b/src/components/skeletons/chart-skeleton.tsx index a42ca00..d62583d 100644 --- a/src/components/skeletons/chart-skeleton.tsx +++ b/src/components/skeletons/chart-skeleton.tsx @@ -1,4 +1,4 @@ -import { Skeleton } from "@/components/ui/skeleton"; +import { Skeleton } from "@ui/skeleton"; export function ChartSkeleton({ height = 300 }: { height?: number }) { return ( diff --git a/src/components/skeletons/section-skeleton.tsx b/src/components/skeletons/section-skeleton.tsx index ecdffa3..efa5386 100644 --- a/src/components/skeletons/section-skeleton.tsx +++ b/src/components/skeletons/section-skeleton.tsx @@ -1,4 +1,4 @@ -import { Skeleton } from "@/components/ui/skeleton"; +import { Skeleton } from "@ui/skeleton"; export function SectionSkeleton() { return ( diff --git a/src/components/skeletons/stats-skeleton.tsx b/src/components/skeletons/stats-skeleton.tsx index fd35041..d1dc0b4 100644 --- a/src/components/skeletons/stats-skeleton.tsx +++ b/src/components/skeletons/stats-skeleton.tsx @@ -1,4 +1,4 @@ -import { Skeleton } from "@/components/ui/skeleton"; +import { Skeleton } from "@ui/skeleton"; export function StatsSkeleton() { return ( diff --git a/src/components/skeletons/table-skeleton.tsx b/src/components/skeletons/table-skeleton.tsx index 2e852cc..958ff54 100644 --- a/src/components/skeletons/table-skeleton.tsx +++ b/src/components/skeletons/table-skeleton.tsx @@ -1,4 +1,4 @@ -import { Skeleton } from "@/components/ui/skeleton"; +import { Skeleton } from "@ui/skeleton"; export function TableSkeleton({ rows = 5 }: { rows?: number }) { return ( diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx index c37e0cf..7de2476 100644 --- a/src/components/ui/alert-dialog.tsx +++ b/src/components/ui/alert-dialog.tsx @@ -4,7 +4,7 @@ import * as React from "react"; import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; import { cn } from "@/lib/utils"; -import { buttonVariants } from "@/components/ui/button"; +import { buttonVariants } from "@ui/button"; function AlertDialog({ ...props diff --git a/src/components/ui/button-group.tsx b/src/components/ui/button-group.tsx index 043802e..d3d9fce 100644 --- a/src/components/ui/button-group.tsx +++ b/src/components/ui/button-group.tsx @@ -2,7 +2,7 @@ import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; -import { Separator } from "@/components/ui/separator"; +import { Separator } from "@ui/separator"; const buttonGroupVariants = cva( "flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2", diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx index 0232552..872c864 100644 --- a/src/components/ui/calendar.tsx +++ b/src/components/ui/calendar.tsx @@ -13,7 +13,7 @@ import { } from "react-day-picker"; import { cn } from "@/lib/utils"; -import { Button, buttonVariants } from "@/components/ui/button"; +import { Button, buttonVariants } from "@ui/button"; function Calendar({ className, diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx index dc1985a..94b7dc2 100644 --- a/src/components/ui/carousel.tsx +++ b/src/components/ui/carousel.tsx @@ -7,7 +7,7 @@ import useEmblaCarousel, { import { ArrowLeft, ArrowRight } from "lucide-react"; import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; +import { Button } from "@ui/button"; type CarouselApi = UseEmblaCarouselType[1]; type UseCarouselParameters = Parameters; diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx index ee7450a..9b79780 100644 --- a/src/components/ui/command.tsx +++ b/src/components/ui/command.tsx @@ -11,7 +11,7 @@ import { DialogDescription, DialogHeader, DialogTitle, -} from "@/components/ui/dialog"; +} from "@ui/dialog"; function Command({ className, diff --git a/src/components/ui/field.tsx b/src/components/ui/field.tsx index ec4a26d..4ec45f4 100644 --- a/src/components/ui/field.tsx +++ b/src/components/ui/field.tsx @@ -4,8 +4,8 @@ import React, { useMemo } from "react"; import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; -import { Label } from "@/components/ui/label"; -import { Separator } from "@/components/ui/separator"; +import { Label } from "@ui/label"; +import { Separator } from "@ui/separator"; function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { return ( diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx index b8a8499..4cdaba5 100644 --- a/src/components/ui/form.tsx +++ b/src/components/ui/form.tsx @@ -14,7 +14,7 @@ import { } from "react-hook-form"; import { cn } from "@/lib/utils"; -import { Label } from "@/components/ui/label"; +import { Label } from "@ui/label"; const Form = FormProvider; diff --git a/src/components/ui/input-group.tsx b/src/components/ui/input-group.tsx index 91cccf5..4bc7571 100644 --- a/src/components/ui/input-group.tsx +++ b/src/components/ui/input-group.tsx @@ -4,9 +4,9 @@ import * as React from "react"; import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; +import { Button } from "@ui/button"; +import { Input } from "@ui/input"; +import { Textarea } from "@ui/textarea"; function InputGroup({ className, ...props }: React.ComponentProps<"div">) { return ( diff --git a/src/components/ui/item.tsx b/src/components/ui/item.tsx index 3d21751..e31751d 100644 --- a/src/components/ui/item.tsx +++ b/src/components/ui/item.tsx @@ -3,7 +3,7 @@ import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; -import { Separator } from "@/components/ui/separator"; +import { Separator } from "@ui/separator"; function ItemGroup({ className, ...props }: React.ComponentProps<"div">) { return ( diff --git a/src/components/ui/pagination.tsx b/src/components/ui/pagination.tsx index b10908a..afbdf76 100644 --- a/src/components/ui/pagination.tsx +++ b/src/components/ui/pagination.tsx @@ -6,7 +6,7 @@ import { } from "lucide-react"; import { cn } from "@/lib/utils"; -import { type Button, buttonVariants } from "@/components/ui/button"; +import { type Button, buttonVariants } from "@ui/button"; function Pagination({ className, ...props }: React.ComponentProps<"nav">) { return ( diff --git a/src/components/ui/resizable-navbar.tsx b/src/components/ui/resizable-navbar.tsx index 1318447..5d900d3 100644 --- a/src/components/ui/resizable-navbar.tsx +++ b/src/components/ui/resizable-navbar.tsx @@ -9,7 +9,7 @@ import { } from "motion/react"; import React, { useRef, useState } from "react"; -import { Logo } from "@/components/common/logo"; +import { Logo } from "@common/branding/logo"; import Link from "next/link"; interface NavbarProps { diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx index 2563071..7ff0b96 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/components/ui/sidebar.tsx @@ -7,23 +7,23 @@ import { PanelLeftIcon } from "lucide-react"; import { useIsMobile } from "@/hooks/use-mobile"; import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Separator } from "@/components/ui/separator"; +import { Button } from "@ui/button"; +import { Input } from "@ui/input"; +import { Separator } from "@ui/separator"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, -} from "@/components/ui/sheet"; -import { Skeleton } from "@/components/ui/skeleton"; +} from "@ui/sheet"; +import { Skeleton } from "@ui/skeleton"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, -} from "@/components/ui/tooltip"; +} from "@ui/tooltip"; const SIDEBAR_COOKIE_NAME = "sidebar_state"; const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; diff --git a/src/components/ui/toggle-group.tsx b/src/components/ui/toggle-group.tsx index c9a3c5a..4aeca32 100644 --- a/src/components/ui/toggle-group.tsx +++ b/src/components/ui/toggle-group.tsx @@ -5,7 +5,7 @@ import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"; import { type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; -import { toggleVariants } from "@/components/ui/toggle"; +import { toggleVariants } from "@ui/toggle"; const ToggleGroupContext = React.createContext< VariantProps & { diff --git a/src/constants/ai.ts b/src/constants/ai.ts new file mode 100644 index 0000000..6e12867 --- /dev/null +++ b/src/constants/ai.ts @@ -0,0 +1,10 @@ +export const MAX_RETRIES = 3; +export const RETRY_DELAY_MS = 1000; +export const RATE_LIMIT_DELAY_MS = 5000; + +export const VALID_FREQUENCIES = [ + "DAILY", + "WEEKLY", + "MONTHLY", + "YEARLY", +] as const; diff --git a/src/constants/budget.ts b/src/constants/budget.ts new file mode 100644 index 0000000..d0dae7d --- /dev/null +++ b/src/constants/budget.ts @@ -0,0 +1,17 @@ +export const BUDGET_THRESHOLDS = [ + { + level: 100, + flag: "threshold_100_alert_sent" as const, + title: "Budget Limit Reached", + }, + { + level: 90, + flag: "threshold_90_alert_sent" as const, + title: "Budget Warning (90%)", + }, + { + level: 70, + flag: "threshold_70_alert_sent" as const, + title: "Budget Alert (70%)", + }, +]; diff --git a/src/constants/colors.ts b/src/constants/colors.ts new file mode 100644 index 0000000..fdf084b --- /dev/null +++ b/src/constants/colors.ts @@ -0,0 +1,22 @@ +export const NAMED_COLORS = [ + { name: "Blue", value: "#3B82F6" }, + { name: "Red", value: "#EF4444" }, + { name: "Green", value: "#10B981" }, + { name: "Amber", value: "#F59E0B" }, + { name: "Purple", value: "#8B5CF6" }, + { name: "Pink", value: "#EC4899" }, + { name: "Cyan", value: "#06B6D4" }, + { name: "Teal", value: "#14B8A6" }, + { name: "Indigo", value: "#6366F1" }, + { name: "Fuchsia", value: "#D946EF" }, + { name: "Lime", value: "#84CC16" }, + { name: "Slate", value: "#64748B" }, + { name: "Orange", value: "#F97316" }, + { name: "Violet", value: "#A855F7" }, + { name: "Sky", value: "#0EA5E9" }, + { name: "Emerald", value: "#22C55E" }, + { name: "Yellow", value: "#EAB308" }, + { name: "Rose", value: "#F43F5E" }, + { name: "Stone", value: "#78716C" }, + { name: "Black", value: "#171717" }, +]; diff --git a/src/constants/defaults.ts b/src/constants/defaults.ts new file mode 100644 index 0000000..c7fa116 --- /dev/null +++ b/src/constants/defaults.ts @@ -0,0 +1,7 @@ +/** + * Default avatar URLs used as fallbacks when no custom avatar is provided. + */ +export const DEFAULT_AVATARS = { + BOY: "https://avatar.iran.liara.run/public/boy", + GIRL: "https://avatar.iran.liara.run/public/girl", +} as const; diff --git a/src/constants/event-schemas.ts b/src/constants/event-schemas.ts new file mode 100644 index 0000000..c13956b --- /dev/null +++ b/src/constants/event-schemas.ts @@ -0,0 +1,26 @@ +import { z } from "zod"; + +export const TransactionProcessedSchema = z.object({ + userId: z.string(), + transactionId: z.string(), + accountId: z.string(), + categoryId: z.string().nullable(), + date: z.coerce.date(), +}); + +export const BudgetThresholdSchema = z.object({ + budgetId: z.string(), + userId: z.string(), + threshold: z.number(), +}); + +export const TransactionAlertSchema = z.object({ + userId: z.string(), + amount: z.number(), + description: z.string(), + threshold: z.number(), +}); + +export const RecurringSchema = z.object({ + ruleId: z.string(), +}); diff --git a/src/lib/inngest/events.ts b/src/constants/events.ts similarity index 73% rename from src/lib/inngest/events.ts rename to src/constants/events.ts index 7add68d..1c8be2f 100644 --- a/src/lib/inngest/events.ts +++ b/src/constants/events.ts @@ -1,9 +1,10 @@ -import { inngest } from "./client"; +import { inngest } from "@/lib/inngest/client"; export const RECURRING_EVENT = "recurring/run"; export const TRANSACTION_PROCESSED_EVENT = "transaction/processed"; export const SEND_EMAIL_EVENT = "notification/send-email"; export const BUDGET_THRESHOLD_REACHED_EVENT = "budget/threshold.reached"; +export const TRANSACTION_ALERT_EVENT = "transaction/large-transaction.detected"; type InngestSendOptions = { delayUntil?: string; @@ -31,6 +32,18 @@ export async function emitTransactionProcessed(params: { }); } +export async function emitTransactionAlert(params: { + userId: string; + amount: number; + description: string; + threshold: number; +}) { + await inngest.send({ + name: TRANSACTION_ALERT_EVENT, + data: params, + }); +} + export async function enqueueEmail(params: { to: string; subject: string; diff --git a/src/lib/format-options.ts b/src/constants/formatting.ts similarity index 94% rename from src/lib/format-options.ts rename to src/constants/formatting.ts index 1914d8b..460be5a 100644 --- a/src/lib/format-options.ts +++ b/src/constants/formatting.ts @@ -1,5 +1,19 @@ import type { Gender, Currency, Country, Timezone } from "@prisma/client"; +export const CURRENCY_SYMBOLS: Record = { + USD: "$", + EUR: "€", + GBP: "£", + JPY: "¥", + AUD: "$", + CAD: "$", + CHF: "Fr", + CNY: "¥", + INR: "₹", + SGD: "$", + PKR: "₨", +}; + export const GenderOptions = { MALE: "Male", FEMALE: "Female", diff --git a/src/constants/icons.ts b/src/constants/icons.ts new file mode 100644 index 0000000..f7c8946 --- /dev/null +++ b/src/constants/icons.ts @@ -0,0 +1,94 @@ +import { + Wallet, + CreditCard, + PiggyBank, + Banknote, + Landmark, + DollarSign, + Coins, + Briefcase, + TrendingUp, + Target, + Home, + Building, + ShoppingBag, + Utensils, + Coffee, + Car, + Plane, + Smartphone, + Laptop, + Wifi, + Film, + Music, + Gamepad, + Dumbbell, + Stethoscope, + GraduationCap, + Book, + Palette, + Camera, + Baby, + Dog, + Zap, + Shield, + Gift, + Clock, + AlertCircle, + CheckCircle, + Heart, + Star, + Lock, + Key, + Wrench, + Sun, +} from "lucide-react"; +import type { LucideIcon } from "lucide-react"; + +export const ICONS: { name: string; Icon: LucideIcon }[] = [ + { name: "wallet", Icon: Wallet }, + { name: "credit-card", Icon: CreditCard }, + { name: "piggy-bank", Icon: PiggyBank }, + { name: "bank", Icon: Banknote }, + { name: "landmark", Icon: Landmark }, + { name: "dollar", Icon: DollarSign }, + { name: "coins", Icon: Coins }, + { name: "briefcase", Icon: Briefcase }, + { name: "trending-up", Icon: TrendingUp }, + { name: "target", Icon: Target }, + { name: "home", Icon: Home }, + { name: "building", Icon: Building }, + { name: "shopping", Icon: ShoppingBag }, + { name: "food", Icon: Utensils }, + { name: "coffee", Icon: Coffee }, + { name: "car", Icon: Car }, + { name: "transport", Icon: Plane }, + { name: "phone", Icon: Smartphone }, + { name: "tech", Icon: Laptop }, + { name: "internet", Icon: Wifi }, + { name: "entertainment", Icon: Film }, + { name: "music", Icon: Music }, + { name: "gaming", Icon: Gamepad }, + { name: "fitness", Icon: Dumbbell }, + { name: "health", Icon: Stethoscope }, + { name: "education", Icon: GraduationCap }, + { name: "books", Icon: Book }, + { name: "art", Icon: Palette }, + { name: "camera", Icon: Camera }, + { name: "kids", Icon: Baby }, + { name: "pets", Icon: Dog }, + { name: "zap", Icon: Zap }, + { name: "shield", Icon: Shield }, + { name: "gift", Icon: Gift }, + { name: "clock", Icon: Clock }, + { name: "alert", Icon: AlertCircle }, + { name: "check", Icon: CheckCircle }, + { name: "heart", Icon: Heart }, + { name: "star", Icon: Star }, + { name: "lock", Icon: Lock }, + { name: "key", Icon: Key }, + { name: "tools", Icon: Wrench }, + { name: "weather", Icon: Sun }, +]; + +export const ICON_MAP = new Map(ICONS.map((i) => [i.name, i.Icon])); diff --git a/src/constants/prompt.ts b/src/constants/prompt.ts index d94f8a4..12136a9 100644 --- a/src/constants/prompt.ts +++ b/src/constants/prompt.ts @@ -48,9 +48,6 @@ Example: **Now categorize all transactions and return the JSON array:**`; -// ============================================================================ -// Budget Recommendation Prompt -// ============================================================================ export const BUDGET_RECOMMENDATION_TEMPLATE = `You are an expert financial advisor AI specializing in budget planning and optimization. Analyze the following user's spending data and existing budgets, then provide personalized budget recommendations to help them manage their finances better. @@ -84,9 +81,6 @@ Return ONLY a valid JSON array (no markdown, no explanation) with 3-5 recommenda **Provide actionable, realistic budget recommendations:**`; -// ============================================================================ -// Spending Insights Prompt -// ============================================================================ export const SPENDING_INSIGHTS_TEMPLATE = `You are a financial data analyst AI. Analyze the user's monthly spending and provide actionable insights. ## Period: {{period}} @@ -126,9 +120,6 @@ Return ONLY valid JSON (no markdown): **Provide data-driven insights:**`; -// ============================================================================ -// Anomaly Detection Prompt -// ============================================================================ export const ANOMALY_DETECTION_TEMPLATE = `You are a fraud detection and spending analysis AI. Identify unusual or anomalous transactions that deviate from normal spending patterns. ## Recent Transactions (last 10): @@ -165,9 +156,6 @@ Return ONLY valid JSON (no markdown): **Detect unusual spending:**`; -// ============================================================================ -// Financial Advice Prompt -// ============================================================================ export const FINANCIAL_ADVICE_TEMPLATE = `You are a certified financial advisor AI providing personalized financial wellness tips. ## User Financial Overview: @@ -202,9 +190,6 @@ Return ONLY a valid JSON array (no markdown): **Provide personalized financial guidance:**`; -// ============================================================================ -// Receipt OCR / Autofill Prompt -// ============================================================================ export const RECEIPT_PROMPT_TEMPLATE = `You are an expert receipt parser and financial data extractor. Given either extracted receipt text or an image URL (if text is not available), return a single JSON object (no markdown, no explanation) with the following shape: diff --git a/src/content/transactions.ts b/src/content/transactions.ts deleted file mode 100644 index 57bb4b4..0000000 --- a/src/content/transactions.ts +++ /dev/null @@ -1,69 +0,0 @@ -export const MAX_IMPORT_LIMIT = 300; -export const MAX_FILE_SIZE = 5 * 1024 * 1024; - -export const CATEGORIES = [ - { value: "groceries", label: "Groceries" }, - { value: "dining", label: "Dining & Restaurants" }, - { value: "transportation", label: "Transportation" }, - { value: "utilities", label: "Utilities" }, - { value: "entertainment", label: "Entertainment" }, - { value: "shopping", label: "Shopping" }, - { value: "healthcare", label: "Healthcare" }, - { value: "travel", label: "Travel" }, - { value: "housing", label: "Housing & Rent" }, - { value: "income", label: "Income" }, - { value: "investments", label: "Investments" }, - { value: "other", label: "Other" }, -]; - -export const PAYMENT_METHODS_ENUM = { - CARD: "CARD", - BANK_TRANSFER: "BANK_TRANSFER", - MOBILE_PAYMENT: "MOBILE_PAYMENT", - CASH: "CASH", - AUTO_DEBIT: "AUTO_DEBIT", - OTHER: "OTHER", -} as const; - -export const PAYMENT_METHODS = [ - { value: PAYMENT_METHODS_ENUM.CARD, label: "Credit/Debit Card" }, - { value: PAYMENT_METHODS_ENUM.CASH, label: "Cash" }, - { value: PAYMENT_METHODS_ENUM.BANK_TRANSFER, label: "Bank Transfer" }, - { value: PAYMENT_METHODS_ENUM.MOBILE_PAYMENT, label: "Mobile Payment" }, - { value: PAYMENT_METHODS_ENUM.AUTO_DEBIT, label: "Auto Debit" }, - { value: PAYMENT_METHODS_ENUM.OTHER, label: "Other" }, -]; - -export const _TRANSACTION_FREQUENCY = { - DAILY: "DAILY", - WEEKLY: "WEEKLY", - MONTHLY: "MONTHLY", - YEARLY: "YEARLY", -} as const; - -export type TransactionFrequencyType = keyof typeof _TRANSACTION_FREQUENCY; - -export const _TRANSACTION_TYPE = { - INCOME: "INCOME", - EXPENSE: "EXPENSE", -} as const; - -export type _TransactionType = keyof typeof _TRANSACTION_TYPE; - -export const _TRANSACTION_STATUS = { - PENDING: "PENDING", - COMPLETED: "COMPLETED", - FAILED: "FAILED", -} as const; - -export type TransactionStatusType = keyof typeof _TRANSACTION_STATUS; - -export const _REPORT_STATUS = { - SENT: "SENT", - FAILED: "FAILED", - PENDING: "PENDING", - PROCESSING: "PROCESSING", - NO_ACTIVITY: "NO_ACTIVITY", -} as const; - -export type ReportStatusType = keyof typeof _REPORT_STATUS; diff --git a/src/env.js b/src/env.js index 89e2f48..4c96a3c 100644 --- a/src/env.js +++ b/src/env.js @@ -28,11 +28,14 @@ export const env = createEnv({ BETTER_AUTH_URL: z.string().url(), INNGEST_EVENT_KEY: z.string().optional(), INNGEST_SIGNING_KEY: z.string().optional(), - PRISMA_ACCELERATE_URL: z.string().url().optional(), IMAGEKIT_PRIVATE_KEY: z.string().optional(), - IMAGEKIT_URL_ENDPOINT: z.string().url().optional(), - RESEND_API_KEY: z.string(), - EMAIL_FROM: z.string().email(), + RESEND_API_KEY: z.string().optional(), + EMAIL_FROM: z.string().email().optional(), + SMTP_HOST: z.string().optional(), + SMTP_PORT: z.coerce.number().optional(), + SMTP_USER: z.string().optional(), + SMTP_PASS: z.string().optional(), + SMTP_FROM: z.string().optional(), UPSTASH_REDIS_REST_URL: z.string().url().optional(), UPSTASH_REDIS_REST_TOKEN: z.string().optional(), }, @@ -68,11 +71,14 @@ export const env = createEnv({ BETTER_AUTH_URL: process.env.BETTER_AUTH_URL, INNGEST_EVENT_KEY: process.env.INNGEST_EVENT_KEY, INNGEST_SIGNING_KEY: process.env.INNGEST_SIGNING_KEY, - PRISMA_ACCELERATE_URL: process.env.PRISMA_ACCELERATE_URL, RESEND_API_KEY: process.env.RESEND_API_KEY, EMAIL_FROM: process.env.EMAIL_FROM, + SMTP_HOST: process.env.SMTP_HOST, + SMTP_PORT: process.env.SMTP_PORT, + SMTP_USER: process.env.SMTP_USER, + SMTP_PASS: process.env.SMTP_PASS, + SMTP_FROM: process.env.SMTP_FROM, IMAGEKIT_PRIVATE_KEY: process.env.IMAGEKIT_PRIVATE_KEY, - IMAGEKIT_URL_ENDPOINT: process.env.IMAGEKIT_URL_ENDPOINT, NEXT_PUBLIC_IMAGEKIT_PUBLIC_KEY: process.env.NEXT_PUBLIC_IMAGEKIT_PUBLIC_KEY, NEXT_PUBLIC_IMAGEKIT_URL_ENDPOINT: diff --git a/src/hooks/use-accounts.ts b/src/hooks/use-accounts.ts index 1bc1398..836ff54 100644 --- a/src/hooks/use-accounts.ts +++ b/src/hooks/use-accounts.ts @@ -1,6 +1,6 @@ import { useCallback } from "react"; import { api } from "@/trpc/react"; -import { invalidateAccounts } from "@/lib/trpc/invalidation"; +import { invalidateAccounts } from "@/trpc/invalidation"; import type { ApiBankAccount } from "@/types/account"; /** diff --git a/src/hooks/use-auth.ts b/src/hooks/use-auth.ts index d16bf40..4bdcc7f 100644 --- a/src/hooks/use-auth.ts +++ b/src/hooks/use-auth.ts @@ -1,10 +1,10 @@ import { useState, useCallback } from "react"; import { authClient } from "@/lib/auth/client"; import type { User } from "@/types/user"; -import { useUserStore } from "@/store/userStore"; -import { toError } from "@/lib/shared/error"; +import { toError } from "@shared/error"; import { isUser } from "@/lib/utils"; import { createLogger } from "@/lib/logging"; +import { api } from "@/trpc/react"; const logger = createLogger("use-auth"); @@ -27,10 +27,11 @@ interface SignInPayload { export function useAuth() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const utils = api.useUtils(); - const user = useUserStore((s) => s.user); - const setPersistedUser = useUserStore((s) => s.setUser); - const clearPersistedUser = useUserStore((s) => s.clear); + const invalidateUserCache = useCallback(() => { + void utils.user.getMe.invalidate(); + }, [utils]); const fetchUser = useCallback(async () => { setLoading(true); @@ -38,8 +39,7 @@ export function useAuth() { const { data: session, error } = await authClient.getSession(); if (error) throw toError(error); const maybeUser = session?.user ?? null; - const finalUser = isUser(maybeUser) ? maybeUser : null; - setPersistedUser(finalUser); + invalidateUserCache(); logger.info("Fetched user session", { user: maybeUser }); } catch (err) { setError(toError(err)); @@ -49,7 +49,7 @@ export function useAuth() { } finally { setLoading(false); } - }, [setPersistedUser]); + }, [invalidateUserCache]); const signUp = useCallback( async (payload: SignUpPayload): Promise => { @@ -68,7 +68,6 @@ export function useAuth() { if (result.error) throw toError(result.error); await fetchUser(); const maybeUser = "user" in result ? result.user : null; - setPersistedUser(isUser(maybeUser) ? maybeUser : null); logger.info("User signed up", { user: maybeUser }); return isUser(maybeUser) ? maybeUser : null; } catch (err) { @@ -82,7 +81,7 @@ export function useAuth() { setLoading(false); } }, - [fetchUser, setPersistedUser], + [fetchUser], ); const signIn = useCallback( @@ -107,7 +106,6 @@ export function useAuth() { if (result.error) throw toError(result.error); await fetchUser(); const maybeUser = "user" in result ? result.user : null; - setPersistedUser(isUser(maybeUser) ? maybeUser : null); logger.info("User signed in", { user: maybeUser }); return isUser(maybeUser) ? maybeUser : null; } catch (err) { @@ -121,15 +119,15 @@ export function useAuth() { setLoading(false); } }, - [fetchUser, setPersistedUser], + [fetchUser], ); const signOut = useCallback(async (): Promise => { setLoading(true); try { - clearPersistedUser(); + utils.user.getMe.setData(undefined, undefined); await authClient.signOut({ - fetchOptions: { onSuccess: () => clearPersistedUser() }, + fetchOptions: { onSuccess: () => invalidateUserCache() }, }); logger.info("User signed out"); } catch (err) { @@ -142,7 +140,7 @@ export function useAuth() { } finally { setLoading(false); } - }, [clearPersistedUser]); + }, [utils, invalidateUserCache]); const sendVerificationEmail = useCallback( async (email: string, callbackURL?: string): Promise => { @@ -231,7 +229,6 @@ export function useAuth() { ); return { - user, loading, error, signUp, diff --git a/src/hooks/use-categories.ts b/src/hooks/use-categories.ts index e4df4e6..dd631b2 100644 --- a/src/hooks/use-categories.ts +++ b/src/hooks/use-categories.ts @@ -2,7 +2,7 @@ import { useMemo } from "react"; import { api } from "@/trpc/react"; -import { invalidateCategories } from "@/lib/trpc/invalidation"; +import { invalidateCategories } from "@/trpc/invalidation"; import type { CategoryWithChildren } from "@/types/category"; export function useCategories() { diff --git a/src/hooks/use-formatter.ts b/src/hooks/use-formatter.ts index 2cb43b9..f958ed8 100644 --- a/src/hooks/use-formatter.ts +++ b/src/hooks/use-formatter.ts @@ -12,7 +12,6 @@ export function useFormatter() { overrideCurrency?: Currency, ) => { if (isLoading || !settings) { - // Return a basic fallback while loading return new Intl.NumberFormat("en-US", { style: "currency", currency: overrideCurrency ?? "USD", diff --git a/src/hooks/use-mobile.ts b/src/hooks/use-mobile.ts index cbc462c..a93d583 100644 --- a/src/hooks/use-mobile.ts +++ b/src/hooks/use-mobile.ts @@ -3,7 +3,9 @@ import * as React from "react"; const MOBILE_BREAKPOINT = 768; export function useIsMobile() { - const [isMobile, setIsMobile] = React.useState(false); + const [isMobile, setIsMobile] = React.useState( + undefined, + ); React.useEffect(() => { const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); @@ -11,10 +13,9 @@ export function useIsMobile() { setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); }; mql.addEventListener("change", onChange); - // Set initial value on mount (client-side only) setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); return () => mql.removeEventListener("change", onChange); }, []); - return isMobile; + return !!isMobile; } diff --git a/src/hooks/use-sessions.ts b/src/hooks/use-sessions.ts index 3d14311..4904901 100644 --- a/src/hooks/use-sessions.ts +++ b/src/hooks/use-sessions.ts @@ -1,6 +1,6 @@ import { useCallback } from "react"; import { api } from "@/trpc/react"; -import { invalidateSessions } from "@/lib/trpc/invalidation"; +import { invalidateSessions } from "@/trpc/invalidation"; import type { SessionItem } from "@/types/session"; export function useSessions() { diff --git a/src/hooks/use-settings.ts b/src/hooks/use-settings.ts index edf1efe..bd1497d 100644 --- a/src/hooks/use-settings.ts +++ b/src/hooks/use-settings.ts @@ -1,7 +1,7 @@ "use client"; import { api } from "@/trpc/react"; -import { invalidateSettings } from "@/lib/trpc/invalidation"; +import { invalidateSettings } from "@/trpc/invalidation"; import { toast } from "sonner"; export function useSettings() { diff --git a/src/hooks/use-transactions.ts b/src/hooks/use-transactions.ts index 18e67cf..e4b00b3 100644 --- a/src/hooks/use-transactions.ts +++ b/src/hooks/use-transactions.ts @@ -1,7 +1,7 @@ "use client"; import { api } from "@/trpc/react"; -import { invalidateTransactions } from "@/lib/trpc/invalidation"; +import { invalidateTransactions } from "@/trpc/invalidation"; export function useTransactions() { const utils = api.useUtils(); diff --git a/src/hooks/use-user.ts b/src/hooks/use-user.ts index 0511537..9507155 100644 --- a/src/hooks/use-user.ts +++ b/src/hooks/use-user.ts @@ -1,21 +1,12 @@ -import { useCallback, useEffect } from "react"; +import { useCallback } from "react"; import { api } from "@/trpc/react"; -import { invalidateUser } from "@/lib/trpc/invalidation"; -import { useUserStore } from "@/store/userStore"; -import type { User, ApiUser } from "@/types/user"; - -function mapApiToUser(apiUser: ApiUser): User { - return { - ...apiUser, - createdAt: new Date(apiUser.createdAt), - updatedAt: new Date(apiUser.updatedAt), - banExpires: apiUser.banExpires ? new Date(apiUser.banExpires) : null, - }; -} +import { invalidateUser } from "@/trpc/invalidation"; +import type { ApiUser } from "@/types/user"; /** * useUser hook * + * Single source of truth for user data via React Query / tRPC. * - reads the current user via `api.user.getMe` * - exposes `updateProfile` and `uploadProfileImage` helpers * - provides a convenience `uploadFile` that accepts a File and performs @@ -26,18 +17,6 @@ export function useUser() { staleTime: 1000 * 60 * 5, }); - const setUser = useUserStore((s) => s.setUser); - - useEffect(() => { - if (!getMe.data) { - setUser(null); - return; - } - - const typedUser = getMe.data as ApiUser; - setUser(mapApiToUser(typedUser)); - }, [getMe.data, setUser]); - const utils = api.useUtils(); const updateProfileMutation = api.user.updateProfile.useMutation({ @@ -53,7 +32,6 @@ export function useUser() { onSuccess: (updatedUser) => { const typedUser = updatedUser as ApiUser; utils.user.getMe.setData(undefined, typedUser); - setUser(mapApiToUser(typedUser)); void invalidateUser(utils); return updatedUser; }, diff --git a/src/lib/auth/index.ts b/src/lib/auth/index.ts index 59d6899..0c321de 100644 --- a/src/lib/auth/index.ts +++ b/src/lib/auth/index.ts @@ -1,8 +1,7 @@ import { betterAuth } from "better-auth"; import { prismaAdapter } from "better-auth/adapters/prisma"; import { db as prisma } from "@/server/db"; -import { sendEmail } from "@/lib/email"; -import { renderTemplate } from "../server/utils"; +import { sendTemplateEmail } from "@/lib/email"; import { admin } from "better-auth/plugins"; import { nextCookies } from "better-auth/next-js"; import { createLogger } from "@/lib/logging"; @@ -18,15 +17,12 @@ export const auth = betterAuth({ minPasswordLength: 8, maxPasswordLength: 64, sendResetPassword: async ({ user, url }) => { - const html = renderTemplate("password-reset", { - name: user.name || user.email, - resetUrl: url, - }); try { - await sendEmail({ + await sendTemplateEmail({ to: user.email, subject: "Reset your password", - html, + template: "password-reset", + data: { name: user.name || user.email, resetUrl: url }, }); logger.info("Password reset email sent", { email: user.email }); } catch (error) { @@ -43,15 +39,12 @@ export const auth = betterAuth({ }, emailVerification: { sendVerificationEmail: async ({ user, url }) => { - const html = renderTemplate("verification", { - name: user.name || user.email, - verificationUrl: url, - }); try { - await sendEmail({ + await sendTemplateEmail({ to: user.email, subject: "Verify your email address", - html, + template: "verification", + data: { name: user.name || user.email, verificationUrl: url }, }); logger.info("Verification email sent", { email: user.email }); } catch (error) { diff --git a/src/lib/email/index.ts b/src/lib/email/index.ts index eeb08f7..fe24850 100644 --- a/src/lib/email/index.ts +++ b/src/lib/email/index.ts @@ -1,5 +1,10 @@ +import nodemailer from "nodemailer"; import Handlebars from "handlebars"; import { getTemplate } from "@/lib/email/template-cache"; +import { env } from "@/env"; +import { createLogger } from "@/lib/logging"; + +const logger = createLogger("email"); interface SendEmailOptions { to: string; @@ -15,41 +20,106 @@ interface SendTemplateEmailOptions { | "monthly-summary" | "weekly-digest" | "transaction-alert" + | "ai-insight" | "verification" | "password-reset"; data: Record; } -export async function sendEmail({ - to, - subject, - html, -}: SendEmailOptions): Promise { - const apiKey = process.env.RESEND_API_KEY; - const from = process.env.EMAIL_FROM; - if (!apiKey || !from) - throw new Error("Missing RESEND_API_KEY or EMAIL_FROM env variable"); - +// --- Resend transport --- +async function sendViaResend( + from: string, + to: string, + subject: string, + html: string, +) { const response = await fetch("https://api.resend.com/emails", { method: "POST", headers: { - Authorization: `Bearer ${apiKey}`, + Authorization: `Bearer ${env.RESEND_API_KEY}`, "Content-Type": "application/json", }, - body: JSON.stringify({ - from, - to, - subject, - html, - }), + body: JSON.stringify({ from, to, subject, html }), }); - if (!response.ok) { const error = await response.text(); - throw new Error(`Resend API error: ${error}`); + throw new Error(`Resend API error (${response.status}): ${error}`); } } +// --- SMTP transport --- +function getSmtpTransporter() { + return nodemailer.createTransport({ + host: env.SMTP_HOST, + port: env.SMTP_PORT ?? 587, + secure: (env.SMTP_PORT ?? 587) === 465, + auth: { user: env.SMTP_USER, pass: env.SMTP_PASS }, + }); +} + +async function sendViaSmtp( + from: string, + to: string, + subject: string, + html: string, +) { + const transporter = getSmtpTransporter(); + await transporter.sendMail({ from, to, subject, html }); +} + +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +// --- Smart sendEmail: try Resend first, fall back to SMTP --- +export async function sendEmail({ + to, + subject, + html, +}: SendEmailOptions): Promise { + if (!EMAIL_REGEX.test(to)) { + logger.error("Invalid email address", { to }); + throw new Error(`Invalid email address: ${to}`); + } + + const hasResend = !!env.RESEND_API_KEY && !!env.EMAIL_FROM; + const hasSmtp = !!env.SMTP_HOST && !!env.SMTP_USER && !!env.SMTP_PASS; + + if (hasResend) { + try { + await sendViaResend(env.EMAIL_FROM!, to, subject, html); + logger.info("Email sent via Resend", { to, subject }); + return; + } catch (err) { + logger.warn("Resend failed, trying SMTP fallback", { + to, + error: err instanceof Error ? err.message : String(err), + }); + } + } + + if (hasSmtp) { + const from = env.SMTP_FROM ?? env.SMTP_USER!; + await sendViaSmtp(from, to, subject, html); + logger.info("Email sent via SMTP", { to, subject }); + return; + } + + throw new Error( + "No email transport configured. Set RESEND_API_KEY or SMTP_HOST.", + ); +} + +/** + * Compile an HTML template file with Handlebars data. + * Use this instead of importing Handlebars directly in workers. + */ +export async function compileTemplate( + templateFile: string, + data: Record, +): Promise { + const source = await getTemplate(templateFile); + return Handlebars.compile(source)(data); +} + /** * Send an email using a template */ @@ -66,10 +136,11 @@ export async function sendTemplateEmail({ const compiledTemplate = Handlebars.compile(templateContent); // Add app URL to data - const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000"; + const appUrl = env.NEXT_PUBLIC_APP_URL; const templateData = { ...data, appUrl, + year: new Date().getFullYear(), }; // Render HTML @@ -139,21 +210,3 @@ export async function sendWeeklyDigest( data, }); } - -export async function sendTransactionAlert( - to: string, - data: { - userName: string; - amount: number; - description: string; - date: string; - category: string; - }, -) { - await sendTemplateEmail({ - to, - subject: `Large Transaction Alert: $${data.amount}`, - template: "transaction-alert", - data, - }); -} diff --git a/src/lib/email/template-cache.ts b/src/lib/email/template-cache.ts index 2e19f71..5b9455a 100644 --- a/src/lib/email/template-cache.ts +++ b/src/lib/email/template-cache.ts @@ -1,17 +1,31 @@ import { readFile } from "fs/promises"; import path from "path"; +const ALLOWED_TEMPLATES = [ + "budget-alert.html", + "monthly-summary.html", + "weekly-digest.html", + "transaction-alert.html", + "ai-insight.html", + "verification.html", + "password-reset.html", +] as const; + const cache = new Map(); export async function getTemplate(templateName: string): Promise { + if ( + !ALLOWED_TEMPLATES.includes( + templateName as (typeof ALLOWED_TEMPLATES)[number], + ) + ) { + throw new Error(`Unknown email template: ${templateName}`); + } + const cached = cache.get(templateName); if (cached) return cached; - const templatePath = path.join( - process.cwd(), - "src/lib/email/templates", - templateName, - ); + const templatePath = path.join(__dirname, "templates", templateName); const content = await readFile(templatePath, "utf-8"); cache.set(templateName, content); return content; diff --git a/src/lib/email/templates/ai-insight.html b/src/lib/email/templates/ai-insight.html index f2256d8..8f3ae04 100644 --- a/src/lib/email/templates/ai-insight.html +++ b/src/lib/email/templates/ai-insight.html @@ -1,86 +1,207 @@ - - + + - + + + + AI Spending Insights + + + +
+ Your personalized spending insights are ready + ͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏ +
+ -