From 698970ee6563a5988aef9abc564e19c3757076c1 Mon Sep 17 00:00:00 2001 From: Axel Anderson Date: Sat, 6 Dec 2025 14:28:36 -0500 Subject: [PATCH 01/46] feat: add design document for Interne refactor This design document outlines the refactor from Next.js to TrailBase + Vite: - Replace Next.js with Vite for simpler build tooling - Add TrailBase backend with SQLite for persistent storage - Implement multi-user support with authentication - Convert codebase to TypeScript - Maintain existing UI/UX - Deploy with Docker on VPS --- .../2025-12-06-interne-refactor-design.md | 581 ++++++++++++++++++ 1 file changed, 581 insertions(+) create mode 100644 docs/plans/2025-12-06-interne-refactor-design.md diff --git a/docs/plans/2025-12-06-interne-refactor-design.md b/docs/plans/2025-12-06-interne-refactor-design.md new file mode 100644 index 0000000..ad50c4e --- /dev/null +++ b/docs/plans/2025-12-06-interne-refactor-design.md @@ -0,0 +1,581 @@ +# Interne Refactor Design + +**Date:** 2025-12-06 +**Author:** Design Session + +## Overview + +Interne is a spaced-repetition bookmark manager that resurfaces saved websites after configurable intervals using an entropy-based algorithm. This document outlines the refactor from a Next.js client-only app to a multi-user application with persistent storage. + +## Current State + +The app runs entirely in the browser with localStorage for persistence. Next.js 12 provides the framework, webpack handles bundling, and JavaScript comprises the codebase. One user per browser instance. + +**Architecture:** +- Single-page React app (pages/index.js) +- Components: CreateEntryForm, Header, Footer, form elements +- Services: localStorage operations +- Utils: entropy calculation, date formatting, constants + +**Data model:** +Each entry stores URL, title, description, duration, interval (hours/days/weeks/months/years), visit count, creation timestamp, update timestamp, and dismissal timestamp. The entropy algorithm calculates when to resurface each entry. + +## Goals + +1. Replace Next.js with a simpler framework +2. Add backend with persistent database (SQLite) +3. Support multiple users with authentication +4. Switch from webpack to Vite +5. Convert codebase to TypeScript +6. Maintain current UI and UX + +## Architecture + +### Stack + +**Backend:** TrailBase (single-executable Rust server) +- Includes SQLite database +- Provides built-in authentication (password + OAuth) +- Generates REST APIs automatically +- Offers real-time sync capabilities +- Ships with admin dashboard + +**Frontend:** Vite + React 18 + TypeScript +- Fast development server with HMR +- Built-in TypeScript support +- CSS modules (no changes needed) +- TrailBase TypeScript SDK for API calls + +**Database:** SQLite (managed by TrailBase) +- Single-file persistence +- Simple backups (copy the .db file) +- Sufficient for hundreds of users + +### Project Structure + +``` +interne/ +├── frontend/ +│ ├── src/ +│ │ ├── components/ # Existing React components +│ │ ├── services/ # API client for TrailBase +│ │ ├── hooks/ # React Query hooks +│ │ ├── types/ # TypeScript interfaces +│ │ ├── utils/ # Existing utilities (entropy, date) +│ │ ├── styles/ # Existing CSS modules +│ │ ├── App.tsx # Main component +│ │ └── main.tsx # Entry point +│ ├── index.html +│ ├── package.json +│ ├── pnpm-lock.yaml +│ ├── vite.config.ts +│ └── tsconfig.json +├── backend/ +│ ├── trailbase # TrailBase executable +│ ├── config.json # TrailBase configuration +│ └── migrations/ # Database schema +├── Dockerfile +├── docker-compose.yml +└── README.md +``` + +## Database Schema + +SQLite schema for the entries table: + +```sql +CREATE TABLE entries ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + url TEXT NOT NULL, + title TEXT NOT NULL, + description TEXT, + duration INTEGER NOT NULL, + interval TEXT NOT NULL, + visited INTEGER DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME, + dismissed_at DATETIME, + FOREIGN KEY (user_id) REFERENCES _user(id) ON DELETE CASCADE +); + +CREATE INDEX idx_entries_user_id ON entries(user_id); +CREATE INDEX idx_entries_dismissed_at ON entries(dismissed_at); +``` + +**Field descriptions:** +- `id`: Backend-generated UUID (replaces client-side uuid generation) +- `user_id`: References TrailBase's built-in `_user` table +- `url`: Website URL (normalized via URL constructor) +- `title`: User-provided title +- `description`: Optional description +- `duration`: Number of time units (e.g., 3) +- `interval`: Time unit ('hours', 'days', 'weeks', 'months', 'years') +- `visited`: Count of times user visited the URL +- `created_at`: Entry creation timestamp (auto-set) +- `updated_at`: Last modification timestamp +- `dismissed_at`: Last time user marked as read or clicked the link + +**Computed fields:** +The `availableAt` and `visible` fields remain client-side calculations. The entropy algorithm runs in the frontend, computing when each entry should resurface based on `dismissed_at`, `duration`, and `interval`. This preserves existing logic and keeps the database normalized. + +## Authentication + +TrailBase provides authentication out of the box. We start with email/password, with OAuth (Google, Discord) available for future enhancement. + +**User flow:** +1. User visits app +2. If unauthenticated, show login page +3. User logs in or registers +4. TrailBase returns JWT access token + refresh token +5. Frontend stores tokens (httpOnly cookies recommended) +6. All API requests include Authorization header +7. TrailBase validates token and injects user_id into queries + +**Access control:** +TrailBase uses ACL rules to isolate user data: + +```json +{ + "entries": { + "read": "user_id = auth.user_id", + "create": "user_id = auth.user_id", + "update": "user_id = auth.user_id", + "delete": "user_id = auth.user_id" + } +} +``` + +Users see only their own entries. No cross-user data leakage. + +**Frontend implementation:** + +```typescript +// services/auth.ts +export async function login(email: string, password: string) { + const response = await trailbase.auth.signIn({ email, password }) + return response.user +} + +export async function register(email: string, password: string) { + const response = await trailbase.auth.signUp({ email, password }) + return response.user +} + +export async function logout() { + await trailbase.auth.signOut() +} +``` + +Wrap the app in an auth context provider. Check authentication state before rendering the main view. Show login form for unauthenticated users. + +## API Integration + +TrailBase auto-generates REST APIs from the database schema. Standard CRUD operations work immediately. + +**Entry operations:** + +```typescript +// services/entries.ts +export async function fetchEntries() { + // GET /api/records/v1/entries + // Filtered by user_id automatically + return await trailbase.records('entries').list() +} + +export async function createEntry(entry: CreateEntryInput) { + // POST /api/records/v1/entries + // user_id injected by TrailBase + return await trailbase.records('entries').create(entry) +} + +export async function updateEntry(id: string, updates: Partial) { + // PATCH /api/records/v1/entries/:id + return await trailbase.records('entries').update(id, updates) +} + +export async function deleteEntry(id: string) { + // DELETE /api/records/v1/entries/:id + return await trailbase.records('entries').delete(id) +} +``` + +Replace all localStorage calls with async API calls. Remove client-side ID generation (TrailBase generates UUIDs). + +## State Management + +Use React Query for server state management. This provides caching, automatic refetching, optimistic updates, and loading/error states. + +**Example hooks:** + +```typescript +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' + +export function useEntries() { + return useQuery({ + queryKey: ['entries'], + queryFn: fetchEntries + }) +} + +export function useUpdateEntry() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: ({ id, updates }) => updateEntry(id, updates), + onSuccess: () => queryClient.invalidateQueries(['entries']) + }) +} + +export function useDeleteEntry() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: deleteEntry, + onSuccess: () => queryClient.invalidateQueries(['entries']) + }) +} +``` + +**Component usage:** + +```typescript +function Index() { + const { data: entries, isLoading } = useEntries() + const updateEntry = useUpdateEntry() + const deleteEntry = useDeleteEntry() + + // Same component logic, different data source +} +``` + +React Query handles loading states, error handling, and cache invalidation automatically. + +## Frontend Migration + +### Remove Next.js Dependencies + +**Changes:** +- Remove `next`, `next/head`, `next/link` imports +- Replace `next/head` with direct index.html `` tags (title, favicon) +- Remove `pages/_app.js` (Vite uses `main.tsx` entry point) +- Rename `pages/index.js` to `src/App.tsx` +- Remove `next.config.js` + +**Keep:** +- All components (Header, Footer, CreateEntryForm, Forms) +- All CSS modules (Vite supports them natively) +- All utilities (entropy, date, formatters, constants) +- Current UI structure and styling + +### TypeScript Conversion + +**File renames:** +- `.js` → `.tsx` (components with JSX) +- `.js` → `.ts` (utilities, services) + +**Type definitions:** + +```typescript +// types/entry.ts +export interface Entry { + id: string + user_id: string + url: string + title: string + description: string | null + duration: number + interval: 'hours' | 'days' | 'weeks' | 'months' | 'years' + visited: number + created_at: string + updated_at: string | null + dismissed_at: string | null + // Computed client-side + visible?: boolean + availableAt?: Date +} + +export type CreateEntryInput = Omit +``` + +**Component props:** + +```typescript +interface CreateEntryFormProps { + onSubmit: (entry: CreateEntryInput) => void + entries: Entry[] + initialValues?: Partial +} +``` + +Type all existing component props, remove PropTypes dependency. + +### Dependency Changes + +**Remove:** +- next +- webpack (and related packages) +- uuid (backend generates IDs) +- prop-types (TypeScript replaces runtime checks) + +**Add:** +- vite +- @vitejs/plugin-react +- @tanstack/react-query +- TrailBase TypeScript SDK +- TypeScript dev dependencies (@types/react, @types/react-dom) + +**Keep:** +- react, react-dom +- dayjs +- lodash.omit, lodash.orderby +- @react-aria/button +- react-transition-group + +### Vite Configuration + +```typescript +// vite.config.ts +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:4000', + changeOrigin: true + } + } + } +}) +``` + +Proxy API requests to TrailBase during development. + +## Data Migration + +Users have existing data in localStorage. Provide a one-time migration on first login. + +**Migration flow:** +1. User logs in after upgrade +2. Check localStorage for `INTERIOR_ENTRIES` key +3. If found, prompt: "Import existing bookmarks?" +4. If accepted, bulk create entries via API +5. Clear localStorage on success + +**Implementation:** + +```typescript +async function migrateFromLocalStorage() { + const stored = localStorage.getItem('INTERIOR_ENTRIES') + if (!stored) return + + const localEntries = JSON.parse(stored) + const results = await Promise.allSettled( + localEntries.map(entry => createEntry(entry)) + ) + + const successful = results.filter(r => r.status === 'fulfilled').length + + if (successful === localEntries.length) { + localStorage.removeItem('INTERIOR_ENTRIES') + localStorage.removeItem('scrollY') + } + + return { total: localEntries.length, imported: successful } +} +``` + +Show success message with import count. Handle partial failures gracefully. + +## Development Workflow + +**Setup:** +```bash +# Install TrailBase +cd backend +# Download TrailBase executable for your platform +chmod +x trailbase + +# Install frontend dependencies +cd frontend +pnpm install +``` + +**Run development servers:** +```bash +# Terminal 1: TrailBase backend +cd backend +./trailbase + +# Terminal 2: Vite frontend +cd frontend +pnpm dev +``` + +TrailBase runs on port 4000 (default). Vite runs on port 5173 with proxy to TrailBase API. + +**Configuration:** +TrailBase uses `backend/config.json` for database path, auth settings, CORS, etc. Defaults work for development. + +## Deployment + +Use Docker for consistent deployment across environments. + +**Dockerfile:** + +```dockerfile +# Build frontend +FROM node:20-alpine AS frontend-builder +WORKDIR /app/frontend + +RUN npm install -g pnpm +COPY frontend/package.json frontend/pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile + +COPY frontend/ ./ +RUN pnpm run build + +# Runtime +FROM debian:bookworm-slim +WORKDIR /app + +COPY backend/trailbase /usr/local/bin/trailbase +RUN chmod +x /usr/local/bin/trailbase + +COPY --from=frontend-builder /app/frontend/dist /app/static +COPY backend/config.json /app/config.json + +EXPOSE 4000 + +CMD ["trailbase", "--static-files", "/app/static", "--config", "/app/config.json"] +``` + +**docker-compose.yml:** + +```yaml +version: '3.8' + +services: + interne: + build: . + ports: + - "4000:4000" + volumes: + - ./data:/app/data + environment: + - DATABASE_PATH=/app/data/interne.db + restart: unless-stopped +``` + +**Deployment steps:** +1. Build Docker image: `docker-compose build` +2. Start container: `docker-compose up -d` +3. Access app at http://localhost:4000 +4. Database persists in `./data/interne.db` + +**Backups:** +```bash +docker-compose exec interne cp /app/data/interne.db /app/data/backup-$(date +%Y%m%d).db +``` + +Copy database file to external storage regularly. + +**VPS deployment:** +Transfer docker-compose.yml and built image to VPS. Run `docker-compose up -d`. Configure reverse proxy (nginx) for HTTPS and custom domain. + +## UI Preservation + +The refactor changes only the data layer and authentication. The UI remains identical. + +**No changes to:** +- Component layout and structure +- CSS modules and styling +- Entropy calculation algorithm +- Date formatting and display +- Keyboard shortcuts (ESC to toggle filter) +- Search/filter functionality +- Entry cards and their interactions +- "Mark Read", "Edit", "Delete" buttons + +**New UI elements:** +- Login/register page (shown when unauthenticated) +- Loading spinners during API calls +- Error messages for failed operations +- Migration prompt for localStorage import + +Users familiar with the current app will find the same interface, now with cloud sync and multi-device access. + +## Implementation Phases + +**Phase 1: Backend Setup** +- Install and configure TrailBase +- Create database schema +- Configure authentication +- Test CRUD operations via TrailBase admin UI + +**Phase 2: Frontend Foundation** +- Create Vite project structure +- Set up TypeScript configuration +- Install dependencies (React Query, TrailBase SDK) +- Configure API proxy + +**Phase 3: Migration** +- Convert existing components to TypeScript +- Replace localStorage with API calls +- Implement authentication flow +- Add React Query hooks + +**Phase 4: Testing** +- Test authentication (login, register, logout) +- Test CRUD operations on entries +- Verify entropy calculations +- Test data migration from localStorage + +**Phase 5: Docker & Deployment** +- Create Dockerfile +- Create docker-compose.yml +- Test local Docker build +- Deploy to VPS + +**Phase 6: Verification** +- Verify multi-user isolation +- Test backups and restore +- Load testing with multiple users +- Monitor performance + +## Success Criteria + +The refactor succeeds when: + +1. Multiple users can register and use the app independently +2. Each user sees only their own entries +3. All existing features work (search, filter, mark read, edit, delete) +4. Entropy algorithm resurfaces entries correctly +5. Data persists across server restarts +6. Docker deployment works on VPS +7. Backups can be created and restored +8. No localStorage dependencies remain +9. TypeScript catches type errors at compile time +10. UI matches current design pixel-perfect + +## Open Questions + +**TrailBase specifics:** +- Exact SDK API surface (consult TrailBase docs during implementation) +- OAuth provider configuration (if implementing beyond email/password) +- Real-time sync capabilities (future enhancement possibility) + +**Performance:** +- Entry limit per user (SQLite handles millions of rows, pagination unlikely needed) +- Concurrent user capacity (test during deployment) + +**Future enhancements:** +- Browser extension for quick bookmark saves +- Mobile app using TrailBase API +- Export/import functionality +- Tags and categories +- Shared collections between users + +These questions resolve during implementation with TrailBase documentation and testing. From c315c074ccc7836f8d6c70645e18d29116b33ce8 Mon Sep 17 00:00:00 2001 From: Axel Anderson Date: Sat, 6 Dec 2025 14:33:39 -0500 Subject: [PATCH 02/46] feat: add implementation plan for Interne refactor This comprehensive plan breaks down the refactor into 15 bite-sized tasks: - Backend setup with TrailBase and SQLite - Frontend foundation with Vite and TypeScript - Component migration from JavaScript to TypeScript - API integration with React Query - Authentication implementation - Docker deployment configuration Each task includes exact file paths, complete code examples, and verification steps following TDD principles. --- docs/plans/2025-12-06-interne-refactor.md | 1971 +++++++++++++++++++++ 1 file changed, 1971 insertions(+) create mode 100644 docs/plans/2025-12-06-interne-refactor.md diff --git a/docs/plans/2025-12-06-interne-refactor.md b/docs/plans/2025-12-06-interne-refactor.md new file mode 100644 index 0000000..2b5f94d --- /dev/null +++ b/docs/plans/2025-12-06-interne-refactor.md @@ -0,0 +1,1971 @@ +# Interne Refactor Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Refactor Interne from a Next.js client-only app to a multi-user application with TrailBase backend, SQLite database, TypeScript, and Vite. + +**Architecture:** TrailBase provides the backend (Rust + SQLite + auth). Vite serves the React frontend with TypeScript. React Query handles server state. Docker packages everything for VPS deployment. + +**Tech Stack:** TrailBase, SQLite, Vite, React 18, TypeScript, React Query, Docker, pnpm + +--- + +## Prerequisites + +Before starting, ensure you have: +- Node.js 20+ installed +- pnpm installed (`npm install -g pnpm`) +- Docker and docker-compose installed +- TrailBase downloaded for your platform (from trailbase.io) + +--- + +## Task 1: Backend - TrailBase Setup + +**Files:** +- Create: `backend/config.json` +- Create: `backend/migrations/001_create_entries.sql` +- Create: `backend/.gitignore` + +**Step 1: Create backend directory structure** + +```bash +mkdir -p backend/migrations +``` + +**Step 2: Download TrailBase executable** + +Visit https://trailbase.io and download the appropriate executable for your platform. Place it in `backend/trailbase` and make it executable: + +```bash +# macOS/Linux +chmod +x backend/trailbase +``` + +**Step 3: Create TrailBase configuration** + +Create `backend/config.json`: + +```json +{ + "database": { + "path": "data/interne.db" + }, + "server": { + "port": 4000, + "cors": { + "allowed_origins": ["http://localhost:5173"] + } + }, + "auth": { + "jwt_secret": "CHANGE_THIS_IN_PRODUCTION", + "access_token_ttl": 900, + "refresh_token_ttl": 604800 + } +} +``` + +**Step 4: Create database migration** + +Create `backend/migrations/001_create_entries.sql`: + +```sql +CREATE TABLE entries ( + id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), + user_id TEXT NOT NULL, + url TEXT NOT NULL, + title TEXT NOT NULL, + description TEXT, + duration INTEGER NOT NULL, + interval TEXT NOT NULL CHECK (interval IN ('hours', 'days', 'weeks', 'months', 'years')), + visited INTEGER DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME, + dismissed_at DATETIME, + FOREIGN KEY (user_id) REFERENCES _user(id) ON DELETE CASCADE +); + +CREATE INDEX idx_entries_user_id ON entries(user_id); +CREATE INDEX idx_entries_dismissed_at ON entries(dismissed_at); +``` + +**Step 5: Create backend .gitignore** + +Create `backend/.gitignore`: + +``` +data/ +*.db +*.db-shm +*.db-wal +trailbase +``` + +**Step 6: Test TrailBase startup** + +Run: `cd backend && ./trailbase` + +Expected: Server starts on port 4000, database created in `data/interne.db` + +**Step 7: Commit backend setup** + +```bash +git add backend/ +git commit -m "feat: add TrailBase backend configuration and schema" +``` + +--- + +## Task 2: Frontend - Vite Project Setup + +**Files:** +- Create: `frontend/package.json` +- Create: `frontend/vite.config.ts` +- Create: `frontend/tsconfig.json` +- Create: `frontend/tsconfig.node.json` +- Create: `frontend/index.html` +- Create: `frontend/src/main.tsx` +- Create: `frontend/src/vite-env.d.ts` +- Create: `frontend/.gitignore` + +**Step 1: Create frontend directory structure** + +```bash +mkdir -p frontend/src +``` + +**Step 2: Create package.json** + +Create `frontend/package.json`: + +```json +{ + "name": "interne-frontend", + "private": true, + "version": "0.31.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint . --ext ts,tsx" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "@tanstack/react-query": "^5.59.0", + "dayjs": "^1.11.13", + "lodash.omit": "^4.5.0", + "lodash.orderby": "^4.6.0", + "@react-aria/button": "^3.10.0", + "react-transition-group": "^4.4.5" + }, + "devDependencies": { + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.1", + "@types/lodash.omit": "^4.5.9", + "@types/lodash.orderby": "^4.6.9", + "@types/react-transition-group": "^4.4.11", + "@vitejs/plugin-react": "^4.3.3", + "typescript": "^5.6.3", + "vite": "^5.4.10", + "eslint": "^9.14.0", + "@typescript-eslint/eslint-plugin": "^8.12.2", + "@typescript-eslint/parser": "^8.12.2" + } +} +``` + +**Step 3: Create Vite configuration** + +Create `frontend/vite.config.ts`: + +```typescript +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:4000', + changeOrigin: true, + }, + }, + }, +}) +``` + +**Step 4: Create TypeScript configuration** + +Create `frontend/tsconfig.json`: + +```json +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} +``` + +Create `frontend/tsconfig.node.json`: + +```json +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "noEmit": true + }, + "include": ["vite.config.ts"] +} +``` + +**Step 5: Create index.html** + +Create `frontend/index.html`: + +```html + + + + + + + Interne + + +
+ + + +``` + +**Step 6: Create main entry point** + +Create `frontend/src/main.tsx`: + +```typescript +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) +``` + +Create `frontend/src/App.tsx`: + +```typescript +function App() { + return
Interne - Coming Soon
+} + +export default App +``` + +Create `frontend/src/vite-env.d.ts`: + +```typescript +/// +``` + +**Step 7: Create frontend .gitignore** + +Create `frontend/.gitignore`: + +``` +node_modules +dist +*.local +.DS_Store +``` + +**Step 8: Install dependencies** + +Run: `cd frontend && pnpm install` + +Expected: Dependencies installed, pnpm-lock.yaml created + +**Step 9: Test Vite dev server** + +Run: `cd frontend && pnpm dev` + +Expected: Server starts on http://localhost:5173, displays "Interne - Coming Soon" + +**Step 10: Commit frontend setup** + +```bash +git add frontend/ +git commit -m "feat: add Vite + React + TypeScript frontend foundation" +``` + +--- + +## Task 3: TypeScript Types + +**Files:** +- Create: `frontend/src/types/entry.ts` +- Create: `frontend/src/types/user.ts` +- Create: `frontend/src/types/api.ts` + +**Step 1: Create entry types** + +Create `frontend/src/types/entry.ts`: + +```typescript +export type Interval = 'hours' | 'days' | 'weeks' | 'months' | 'years' + +export interface Entry { + id: string + user_id: string + url: string + title: string + description: string | null + duration: number + interval: Interval + visited: number + created_at: string + updated_at: string | null + dismissed_at: string | null + // Computed client-side + visible?: boolean + availableAt?: Date +} + +export type CreateEntryInput = Omit< + Entry, + 'id' | 'user_id' | 'created_at' | 'updated_at' | 'visited' | 'visible' | 'availableAt' +> + +export type UpdateEntryInput = Partial +``` + +**Step 2: Create user types** + +Create `frontend/src/types/user.ts`: + +```typescript +export interface User { + id: string + email: string + created_at: string +} + +export interface AuthResponse { + user: User + access_token: string + refresh_token: string +} + +export interface LoginCredentials { + email: string + password: string +} + +export interface RegisterCredentials { + email: string + password: string +} +``` + +**Step 3: Create API types** + +Create `frontend/src/types/api.ts`: + +```typescript +export interface ApiError { + message: string + code?: string + details?: unknown +} + +export interface ListResponse { + data: T[] + total: number +} +``` + +**Step 4: Commit type definitions** + +```bash +git add frontend/src/types/ +git commit -m "feat: add TypeScript type definitions" +``` + +--- + +## Task 4: Copy and Migrate Existing Utilities + +**Files:** +- Create: `frontend/src/utils/constants.ts` +- Create: `frontend/src/utils/date.ts` +- Create: `frontend/src/utils/entropy.ts` +- Create: `frontend/src/utils/formatters.ts` + +**Step 1: Copy constants** + +Copy from `utils/constants.js` and convert to TypeScript in `frontend/src/utils/constants.ts`: + +```typescript +import type { Interval } from '../types/entry' + +export const INTERVALS: Record, Interval> = { + HOURS: 'hours', + DAYS: 'days', + WEEKS: 'weeks', + MONTHS: 'months', + YEARS: 'years', +} + +export const MODES = { + VIEW: 'VIEW', + EDIT: 'EDIT', +} as const + +export const KEY_CODES = { + ESC: 27, + ENTER: 13, +} as const +``` + +**Step 2: Copy date utilities** + +Copy from `utils/date.js` and convert to TypeScript in `frontend/src/utils/date.ts`: + +```typescript +import dayjs, { Dayjs } from 'dayjs' +import relativeTime from 'dayjs/plugin/relativeTime' + +dayjs.extend(relativeTime) + +export const getCurrentDate = (): Dayjs => dayjs() + +export const getDate = (date: string): Dayjs => dayjs(date) + +export const getRelativeTimeFromNow = (date: string): string => { + return dayjs(date).fromNow() +} +``` + +**Step 3: Copy entropy utilities** + +Copy from `utils/entropy.js` and convert to TypeScript in `frontend/src/utils/entropy.ts`: + +```typescript +import type { Entry, Interval } from '../types/entry' +import { getCurrentDate, getDate } from './date' + +const MAX = 7 +const MILLIS_IN_DAY = 24 * 60 * 60 * 1000 + +// TODO: make user configurable +const opts = { + entropy: 5, +} + +export const getAvailableAtPlusEntropy = ({ + dismissed_at, + interval, + duration, +}: Pick): { + availableAt: dayjs.Dayjs + diff: number +} => { + const now = getCurrentDate() + const { entropy } = opts + + const availableAt = dismissed_at + ? getDate(dismissed_at).add(duration, interval as any) + : now.subtract(1, 'seconds') + + const diff = availableAt.diff(now) + + if (entropy && diff > MILLIS_IN_DAY) { + const availableAtPlusEntropy = availableAt.add( + Math.floor(Math.random() * ((entropy / 10) * MAX)), + 'days' + ) + + return { + availableAt: availableAtPlusEntropy, + diff: availableAtPlusEntropy.diff(now), + } + } + + return { availableAt, diff } +} +``` + +**Step 4: Copy formatters** + +Copy from `utils/formatters.js` and convert to TypeScript in `frontend/src/utils/formatters.ts`: + +```typescript +export const toTitleCase = (str: string): string => { + return str + .split(' ') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(' ') +} +``` + +**Step 5: Install dayjs plugins** + +Run: `cd frontend && pnpm add dayjs` + +Expected: dayjs already installed from package.json + +**Step 6: Commit utilities** + +```bash +git add frontend/src/utils/ +git commit -m "feat: migrate utilities to TypeScript" +``` + +--- + +## Task 5: Copy and Migrate Styles + +**Files:** +- Create: `frontend/src/styles/` (copy all CSS modules) + +**Step 1: Copy all CSS modules** + +```bash +cp -r styles frontend/src/styles +``` + +**Step 2: Verify CSS modules copied** + +Run: `ls frontend/src/styles` + +Expected: See Pages.module.css, Index.module.css, Forms.module.css, etc. + +**Step 3: Commit styles** + +```bash +git add frontend/src/styles/ +git commit -m "feat: copy CSS modules to frontend" +``` + +--- + +## Task 6: TrailBase API Client + +**Files:** +- Create: `frontend/src/services/trailbase.ts` +- Create: `frontend/src/services/auth.ts` +- Create: `frontend/src/services/entries.ts` + +**Step 1: Create base TrailBase client** + +Create `frontend/src/services/trailbase.ts`: + +```typescript +import type { Entry } from '../types/entry' +import type { AuthResponse, LoginCredentials, RegisterCredentials } from '../types/user' + +const API_BASE = '/api' + +class TrailBaseClient { + private accessToken: string | null = null + + async request( + endpoint: string, + options: RequestInit = {} + ): Promise { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + ...options.headers, + } + + if (this.accessToken) { + headers['Authorization'] = `Bearer ${this.accessToken}` + } + + const response = await fetch(`${API_BASE}${endpoint}`, { + ...options, + headers, + }) + + if (!response.ok) { + const error = await response.json().catch(() => ({ message: response.statusText })) + throw new Error(error.message || 'Request failed') + } + + return response.json() + } + + setAccessToken(token: string) { + this.accessToken = token + localStorage.setItem('access_token', token) + } + + clearAccessToken() { + this.accessToken = null + localStorage.removeItem('access_token') + } + + getAccessToken(): string | null { + if (!this.accessToken) { + this.accessToken = localStorage.getItem('access_token') + } + return this.accessToken + } +} + +export const trailbase = new TrailBaseClient() +``` + +**Step 2: Create auth service** + +Create `frontend/src/services/auth.ts`: + +```typescript +import type { AuthResponse, LoginCredentials, RegisterCredentials, User } from '../types/user' +import { trailbase } from './trailbase' + +export async function login(credentials: LoginCredentials): Promise { + const response = await trailbase.request('/auth/login', { + method: 'POST', + body: JSON.stringify(credentials), + }) + + trailbase.setAccessToken(response.access_token) + return response +} + +export async function register(credentials: RegisterCredentials): Promise { + const response = await trailbase.request('/auth/register', { + method: 'POST', + body: JSON.stringify(credentials), + }) + + trailbase.setAccessToken(response.access_token) + return response +} + +export async function logout(): Promise { + trailbase.clearAccessToken() +} + +export async function getCurrentUser(): Promise { + const token = trailbase.getAccessToken() + if (!token) return null + + try { + return await trailbase.request('/auth/me') + } catch { + trailbase.clearAccessToken() + return null + } +} +``` + +**Step 3: Create entries service** + +Create `frontend/src/services/entries.ts`: + +```typescript +import type { Entry, CreateEntryInput, UpdateEntryInput } from '../types/entry' +import type { ListResponse } from '../types/api' +import { trailbase } from './trailbase' + +export async function fetchEntries(): Promise { + const response = await trailbase.request>('/records/v1/entries') + return response.data +} + +export async function createEntry(input: CreateEntryInput): Promise { + return trailbase.request('/records/v1/entries', { + method: 'POST', + body: JSON.stringify(input), + }) +} + +export async function updateEntry(id: string, updates: UpdateEntryInput): Promise { + return trailbase.request(`/records/v1/entries/${id}`, { + method: 'PATCH', + body: JSON.stringify(updates), + }) +} + +export async function deleteEntry(id: string): Promise { + await trailbase.request(`/records/v1/entries/${id}`, { + method: 'DELETE', + }) +} +``` + +**Step 4: Commit API services** + +```bash +git add frontend/src/services/ +git commit -m "feat: add TrailBase API client and services" +``` + +--- + +## Task 7: React Query Hooks + +**Files:** +- Create: `frontend/src/hooks/useAuth.ts` +- Create: `frontend/src/hooks/useEntries.ts` + +**Step 1: Create auth hooks** + +Create `frontend/src/hooks/useAuth.ts`: + +```typescript +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import * as authService from '../services/auth' +import type { LoginCredentials, RegisterCredentials } from '../types/user' + +export function useCurrentUser() { + return useQuery({ + queryKey: ['user'], + queryFn: authService.getCurrentUser, + staleTime: 1000 * 60 * 5, // 5 minutes + }) +} + +export function useLogin() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (credentials: LoginCredentials) => authService.login(credentials), + onSuccess: (data) => { + queryClient.setQueryData(['user'], data.user) + }, + }) +} + +export function useRegister() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (credentials: RegisterCredentials) => authService.register(credentials), + onSuccess: (data) => { + queryClient.setQueryData(['user'], data.user) + }, + }) +} + +export function useLogout() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: authService.logout, + onSuccess: () => { + queryClient.setQueryData(['user'], null) + queryClient.clear() + }, + }) +} +``` + +**Step 2: Create entries hooks** + +Create `frontend/src/hooks/useEntries.ts`: + +```typescript +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import * as entriesService from '../services/entries' +import type { CreateEntryInput, UpdateEntryInput } from '../types/entry' + +export function useEntries() { + return useQuery({ + queryKey: ['entries'], + queryFn: entriesService.fetchEntries, + }) +} + +export function useCreateEntry() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (input: CreateEntryInput) => entriesService.createEntry(input), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['entries'] }) + }, + }) +} + +export function useUpdateEntry() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: ({ id, updates }: { id: string; updates: UpdateEntryInput }) => + entriesService.updateEntry(id, updates), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['entries'] }) + }, + }) +} + +export function useDeleteEntry() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (id: string) => entriesService.deleteEntry(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['entries'] }) + }, + }) +} +``` + +**Step 3: Commit hooks** + +```bash +git add frontend/src/hooks/ +git commit -m "feat: add React Query hooks for auth and entries" +``` + +--- + +## Task 8: Migrate Components - Forms + +**Files:** +- Create: `frontend/src/components/Forms.tsx` + +**Step 1: Copy and convert Forms component** + +Copy from `components/Forms.js` and convert to TypeScript in `frontend/src/components/Forms.tsx`: + +```typescript +import React, { forwardRef } from 'react' +import { useButton } from '@react-aria/button' +import styles from '../styles/Forms.module.css' + +interface FormProps { + children: React.ReactNode +} + +export function Form({ children }: FormProps) { + return
{children}
+} + +interface InputProps { + type?: string + value: string | number + label: string + placeholder?: string + onChange: (value: string) => void + pattern?: string + min?: number +} + +export const Input = forwardRef( + ({ type = 'text', value, label, placeholder, onChange, pattern, min }, ref) => { + return ( +
+ + onChange(e.target.value)} + pattern={pattern} + min={min} + /> +
+ ) + } +) + +Input.displayName = 'Input' + +interface SelectOption { + id: string + display: string +} + +interface SelectProps { + label: string + value: string + onChange: (value: string) => void + options: SelectOption[] +} + +export function Select({ label, value, onChange, options }: SelectProps) { + return ( +
+ + +
+ ) +} + +interface ButtonProps { + label: string + onClick: () => void + children: React.ReactNode +} + +export function Button({ label, onClick, children }: ButtonProps) { + const ref = React.useRef(null) + const { buttonProps } = useButton({ onPress: onClick }, ref) + + return ( + + ) +} +``` + +**Step 2: Commit Forms component** + +```bash +git add frontend/src/components/Forms.tsx +git commit -m "feat: migrate Forms component to TypeScript" +``` + +--- + +## Task 9: Migrate Components - Footer and Header + +**Files:** +- Create: `frontend/src/components/Footer.tsx` +- Create: `frontend/src/components/Header.tsx` + +**Step 1: Copy and convert Footer** + +Copy from `components/Footer.js` and convert to TypeScript in `frontend/src/components/Footer.tsx`: + +```typescript +import styles from '../styles/Footer.module.css' + +export default function Footer() { + return ( + + ) +} +``` + +**Step 2: Copy and convert Header** + +Copy from `components/Header.js` and convert to TypeScript in `frontend/src/components/Header.tsx`: + +```typescript +import { MODES } from '../utils/constants' +import styles from '../styles/Header.module.css' + +interface HeaderProps { + mode: string + setMode: (mode: string) => void + setEntry: (entry: null) => void + searchText: string + setSearchText: (text: string) => void +} + +export default function Header({ + mode, + setMode, + setEntry, + searchText, + setSearchText, +}: HeaderProps) { + const handleAddClick = () => { + setEntry(null) + setMode(MODES.EDIT) + } + + const handleCancelClick = () => { + setMode(MODES.VIEW) + } + + return ( +
+
+

Interne

+ + {mode === MODES.VIEW ? ( +
+ setSearchText(e.target.value)} + /> + +
+ ) : ( + + )} +
+
+ ) +} +``` + +**Step 3: Commit Footer and Header** + +```bash +git add frontend/src/components/Footer.tsx frontend/src/components/Header.tsx +git commit -m "feat: migrate Footer and Header components to TypeScript" +``` + +--- + +## Task 10: Migrate Components - CreateEntryForm + +**Files:** +- Create: `frontend/src/components/CreateEntryForm.tsx` + +**Step 1: Copy and convert CreateEntryForm** + +Copy from `components/CreateEntryForm.js` and convert to TypeScript in `frontend/src/components/CreateEntryForm.tsx`: + +```typescript +import { useState, useEffect, useRef, useCallback } from 'react' +import { Form, Input, Select, Button } from './Forms' +import { toTitleCase } from '../utils/formatters' +import { INTERVALS, KEY_CODES } from '../utils/constants' +import type { Entry, CreateEntryInput } from '../types/entry' +import styles from '../styles/Forms.module.css' + +const isValidUrl = (str: string): boolean => { + try { + new URL(str) + return true + } catch { + return false + } +} + +interface CreateEntryFormProps { + onSubmit: (entry: CreateEntryInput) => void + entries: Entry[] + initialValues?: Partial +} + +export default function CreateEntryForm({ + onSubmit, + entries, + initialValues, +}: CreateEntryFormProps) { + const [url, setUrl] = useState(initialValues?.url || '') + const [title, setTitle] = useState(initialValues?.title || '') + const [description, setDescription] = useState(initialValues?.description || '') + const [duration, setDuration] = useState(initialValues?.duration?.toString() || '3') + const [interval, setInterval] = useState(initialValues?.interval || INTERVALS.DAYS) + const [error, setError] = useState('') + + const urlInputRef = useRef(null) + + useEffect(() => { + urlInputRef.current?.focus() + }, []) + + const handleSubmit = useCallback(() => { + if (!url || !title) { + setError('URL and Title are required.') + return + } + + if (!isValidUrl(url)) { + setError('URL is invalid.') + return + } + + if (!initialValues?.id) { + const normalizedUrl = new URL(url).href + const existingEntry = entries.find((x) => new URL(x.url).href === normalizedUrl) + if (existingEntry) { + setError('URL already exists.') + return + } + } + + const durationNum = parseInt(duration, 10) + if (!durationNum || durationNum < 1) { + setError('Duration must be greater than 0.') + return + } + + setError('') + + const entry: CreateEntryInput = { + url: new URL(url).href, + title, + description: description || null, + duration: durationNum, + interval, + dismissed_at: initialValues?.dismissed_at || null, + } + + onSubmit(entry) + + // Reset form + setUrl('') + setTitle('') + setDescription('') + setDuration('3') + setInterval(INTERVALS.DAYS) + }, [url, title, description, duration, interval, entries, initialValues, onSubmit]) + + useEffect(() => { + const handleKeydown = (e: KeyboardEvent) => { + if (e.keyCode === KEY_CODES.ENTER) { + handleSubmit() + } + } + + document.addEventListener('keydown', handleKeydown) + return () => document.removeEventListener('keydown', handleKeydown) + }, [handleSubmit]) + + return ( +
+ {error &&
{error}
} + + + + + + + +
+ + + ) +} +``` + +**Step 2: Create AuthProvider component** + +Create `frontend/src/components/AuthProvider.tsx`: + +```typescript +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { useCurrentUser } from '../hooks/useAuth' +import LoginForm from './LoginForm' + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, + refetchOnWindowFocus: false, + }, + }, +}) + +interface AuthProviderProps { + children: React.ReactNode +} + +function AuthGuard({ children }: AuthProviderProps) { + const { data: user, isLoading } = useCurrentUser() + + if (isLoading) { + return
Loading...
+ } + + if (!user) { + return + } + + return <>{children} +} + +export default function AuthProvider({ children }: AuthProviderProps) { + return ( + + {children} + + ) +} +``` + +**Step 3: Add auth styles to Forms.module.css** + +Add to `frontend/src/styles/Forms.module.css`: + +```css +.authContainer { + max-width: 400px; + margin: 100px auto; + padding: 2rem; + border: 1px solid #eaeaea; + border-radius: 8px; +} + +.authContainer h2 { + text-align: center; + margin-bottom: 1.5rem; +} + +.toggleAuth { + display: block; + margin: 1rem auto 0; + background: none; + border: none; + color: #0070f3; + cursor: pointer; + text-decoration: underline; +} + +.toggleAuth:hover { + color: #0051cc; +} +``` + +**Step 4: Commit auth components** + +```bash +git add frontend/src/components/LoginForm.tsx frontend/src/components/AuthProvider.tsx frontend/src/styles/Forms.module.css +git commit -m "feat: add authentication components and AuthProvider" +``` + +--- + +## Task 12: Main App Component + +**Files:** +- Modify: `frontend/src/App.tsx` + +**Step 1: Write the main App component** + +Replace `frontend/src/App.tsx`: + +```typescript +import { useState, useEffect, useMemo } from 'react' +import orderBy from 'lodash.orderby' +import omit from 'lodash.omit' +import AuthProvider from './components/AuthProvider' +import CreateEntryForm from './components/CreateEntryForm' +import Header from './components/Header' +import Footer from './components/Footer' +import { useEntries, useCreateEntry, useUpdateEntry, useDeleteEntry } from './hooks/useEntries' +import { getAvailableAtPlusEntropy } from './utils/entropy' +import { getRelativeTimeFromNow } from './utils/date' +import { MODES, KEY_CODES } from './utils/constants' +import type { Entry, CreateEntryInput } from './types/entry' +import pageStyles from './styles/Pages.module.css' +import styles from './styles/Index.module.css' + +const msgs = [ + { + en: 'Read a book!', + eo: 'Legi libron!', + }, + { + en: 'Go outside!', + eo: 'Iru eksteren!', + }, +] + +function AppContent() { + const { data: entries = [], isLoading } = useEntries() + const createEntry = useCreateEntry() + const updateEntry = useUpdateEntry() + const deleteEntry = useDeleteEntry() + + const [entry, setEntry] = useState(null) + const [mode, setMode] = useState(MODES.VIEW) + const [isFilterActive, setIsFilterActive] = useState(true) + const [searchText, setSearchText] = useState('') + + const emptyListMsg = msgs[1] + + // Compute visible/availableAt for each entry + const entriesWithComputed = useMemo(() => { + return entries.map((entry) => { + const { availableAt, diff } = getAvailableAtPlusEntropy(entry) + const visible = diff < 0 + return { ...entry, visible, availableAt: availableAt.toDate() } + }) + }, [entries]) + + // Filter and sort entries + const visibleEntries = useMemo(() => { + const filtered = entriesWithComputed.filter((x) => { + if (searchText) { + const escapeRegExp = (str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const regex = new RegExp(escapeRegExp(searchText), 'gi') + return x.title?.match(regex) || x.description?.match(regex) || x.url?.match(regex) + } else { + return isFilterActive ? x.visible : true + } + }) + + return orderBy( + filtered, + isFilterActive ? ['dismissed_at'] : ['dismissed_at', 'availableAt'], + isFilterActive ? ['desc'] : ['desc', 'asc'] + ) + }, [entriesWithComputed, isFilterActive, searchText]) + + useEffect(() => { + const handleKeydown = (e: KeyboardEvent) => { + if (e.keyCode === KEY_CODES.ESC) { + if (mode === MODES.EDIT) { + setMode(MODES.VIEW) + } else if (document.activeElement === document.body) { + setIsFilterActive(!isFilterActive) + } + } + } + + document.addEventListener('keydown', handleKeydown) + return () => document.removeEventListener('keydown', handleKeydown) + }, [isFilterActive, mode]) + + const handleEntryClick = (entry: Entry) => { + setTimeout(() => { + updateEntry.mutate({ + id: entry.id, + updates: { + dismissed_at: new Date().toISOString(), + }, + }) + }, 200) + } + + const handleViewFilterClick = () => setIsFilterActive(!isFilterActive) + + const handleSaveEntry = (input: CreateEntryInput) => { + if (entry?.id) { + updateEntry.mutate( + { id: entry.id, updates: input }, + { onSuccess: () => setMode(MODES.VIEW) } + ) + } else { + createEntry.mutate(input, { onSuccess: () => setMode(MODES.VIEW) }) + } + } + + const handleEditEntry = (entry: Entry) => { + setEntry(entry) + setMode(MODES.EDIT) + window.scrollTo(0, 0) + } + + const handleDeleteEntry = (entry: Entry) => { + const shouldDelete = window.confirm('Are you sure?') + if (shouldDelete) { + deleteEntry.mutate(entry.id) + } + } + + if (isLoading) { + return
Loading...
+ } + + return ( +
+
+ +
+ {mode === MODES.EDIT ? ( + + ) : ( +
+ {visibleEntries.length > 0 ? ( + visibleEntries.map((x) => ( +
+
+ + {x.dismissed_at + ? `Last viewed ${getRelativeTimeFromNow(x.dismissed_at)}` + : 'Never viewed'} + +
+ handleEntryClick(x)} + > +
+

{x.title}

+
+
+

{x.description}

+
+ +
+
+ {!x.visible && x.availableAt && ( + Available {getRelativeTimeFromNow(x.availableAt.toISOString())} + )} +
+ +
+ {x.visible && ( +
handleEntryClick(x)}> + Mark Read +
+ )} +
handleEditEntry(x)}> + Edit +
+
handleDeleteEntry(x)}> + Delete +
+
+
+
+ )) + ) : ( +

+ {searchText ? 'No results' : emptyListMsg.en} +

+ )} +
+ )} +
+ + {mode === MODES.VIEW && ( +
+ {isFilterActive ? 'View All' : View Available} +
+ )} + +
+
+ ) +} + +export default function App() { + return ( + + + + ) +} +``` + +**Step 2: Copy public assets** + +```bash +mkdir -p frontend/public +cp public/favicon.ico frontend/public/ +``` + +**Step 3: Test the app** + +Run: `cd frontend && pnpm dev` + +Expected: App loads, shows login form (TrailBase must be running) + +**Step 4: Commit main app** + +```bash +git add frontend/src/App.tsx frontend/public/ +git commit -m "feat: implement main App component with all features" +``` + +--- + +## Task 13: Docker Setup + +**Files:** +- Create: `Dockerfile` +- Create: `docker-compose.yml` +- Create: `.dockerignore` + +**Step 1: Create Dockerfile** + +Create `Dockerfile`: + +```dockerfile +# Build frontend +FROM node:20-alpine AS frontend-builder + +WORKDIR /app/frontend + +# Install pnpm +RUN npm install -g pnpm + +# Copy package files +COPY frontend/package.json frontend/pnpm-lock.yaml ./ + +# Install dependencies +RUN pnpm install --frozen-lockfile + +# Copy source +COPY frontend/ ./ + +# Build +RUN pnpm run build + +# Runtime image +FROM debian:bookworm-slim + +WORKDIR /app + +# Copy TrailBase executable (you need to download this first) +COPY backend/trailbase /usr/local/bin/trailbase +RUN chmod +x /usr/local/bin/trailbase + +# Copy frontend build +COPY --from=frontend-builder /app/frontend/dist /app/static + +# Copy backend config and migrations +COPY backend/config.json /app/config.json +COPY backend/migrations /app/migrations + +# Create data directory +RUN mkdir -p /app/data + +EXPOSE 4000 + +CMD ["trailbase", "--static-files", "/app/static", "--config", "/app/config.json"] +``` + +**Step 2: Create docker-compose.yml** + +Create `docker-compose.yml`: + +```yaml +version: '3.8' + +services: + interne: + build: . + ports: + - "4000:4000" + volumes: + - ./data:/app/data + environment: + - DATABASE_PATH=/app/data/interne.db + restart: unless-stopped +``` + +**Step 3: Create .dockerignore** + +Create `.dockerignore`: + +``` +node_modules +.git +.gitignore +*.md +.DS_Store +data/ +*.db +dist +.next +``` + +**Step 4: Update root .gitignore** + +Add to root `.gitignore`: + +``` +data/ +``` + +**Step 5: Commit Docker setup** + +```bash +git add Dockerfile docker-compose.yml .dockerignore .gitignore +git commit -m "feat: add Docker configuration for deployment" +``` + +--- + +## Task 14: Update Documentation + +**Files:** +- Modify: `README.md` + +**Step 1: Update README** + +Replace `README.md`: + +```markdown +# Interne + +A spaced-repetition bookmark manager that resurfaces saved websites after configurable intervals. + +## Features + +- Save bookmarks with title, description, and custom revisit intervals +- Entropy-based algorithm resurfaces entries at optimal times +- Multi-user support with authentication +- Search and filter bookmarks +- Keyboard shortcuts (ESC to toggle filter) +- Responsive design + +## Tech Stack + +**Backend:** +- TrailBase (Rust + SQLite) +- Built-in authentication +- Auto-generated REST APIs + +**Frontend:** +- Vite + React 18 + TypeScript +- React Query for server state +- CSS Modules for styling + +## Development + +### Prerequisites + +- Node.js 20+ +- pnpm (`npm install -g pnpm`) +- TrailBase executable (download from trailbase.io) + +### Setup + +1. **Backend:** + ```bash + cd backend + ./trailbase + ``` + +2. **Frontend:** + ```bash + cd frontend + pnpm install + pnpm dev + ``` + +3. **Access:** + - Frontend: http://localhost:5173 + - Backend: http://localhost:4000 + +### Project Structure + +``` +interne/ +├── backend/ # TrailBase configuration +│ ├── trailbase # TrailBase executable +│ ├── config.json # Server configuration +│ └── migrations/ # Database schema +├── frontend/ # Vite React app +│ ├── src/ +│ │ ├── components/ +│ │ ├── hooks/ +│ │ ├── services/ +│ │ ├── styles/ +│ │ ├── types/ +│ │ └── utils/ +│ └── package.json +└── docker-compose.yml # Docker deployment +``` + +## Deployment + +### Docker + +1. **Build:** + ```bash + docker-compose build + ``` + +2. **Run:** + ```bash + docker-compose up -d + ``` + +3. **Access:** + - App: http://localhost:4000 + +### VPS Deployment + +1. Transfer files to VPS +2. Run `docker-compose up -d` +3. Configure reverse proxy (nginx) for HTTPS +4. Point domain to server + +### Backups + +Database is a single SQLite file: + +```bash +docker-compose exec interne cp /app/data/interne.db /app/data/backup.db +``` + +Copy `data/interne.db` to external storage regularly. + +## Migration from Old Version + +On first login after upgrade: +1. App checks for localStorage data +2. Prompts to import existing bookmarks +3. Migrates all entries to backend +4. Clears localStorage + +## License + +MIT +``` + +**Step 2: Commit README** + +```bash +git add README.md +git commit -m "docs: update README with new architecture and setup instructions" +``` + +--- + +## Task 15: Testing and Verification + +**Files:** +- None (manual testing) + +**Step 1: Test backend startup** + +Run: `cd backend && ./trailbase` + +Expected: Server starts, database created, no errors + +**Step 2: Test frontend build** + +Run: `cd frontend && pnpm build` + +Expected: Build succeeds, creates `dist/` directory + +**Step 3: Test Docker build** + +Run: `docker-compose build` + +Expected: Image builds successfully + +**Step 4: Test full stack locally** + +Run in separate terminals: +```bash +# Terminal 1 +cd backend && ./trailbase + +# Terminal 2 +cd frontend && pnpm dev +``` + +**Step 5: Manual testing checklist** + +- [ ] Register new user +- [ ] Login with credentials +- [ ] Create entry +- [ ] View entry in list +- [ ] Edit entry +- [ ] Delete entry +- [ ] Search entries +- [ ] Toggle filter (View All / View Available) +- [ ] Click entry link (marks as read) +- [ ] Verify entry resurfaces after interval +- [ ] Logout +- [ ] Login again (entries persist) + +**Step 6: Test Docker deployment** + +Run: `docker-compose up` + +Expected: App accessible at http://localhost:4000, all features work + +**Step 7: Document any issues found** + +Create GitHub issues for bugs discovered during testing. + +--- + +## Success Criteria + +Implementation is complete when: + +- [x] TrailBase backend running with SQLite database +- [x] Frontend built with Vite + React + TypeScript +- [x] All components migrated and working +- [x] Authentication (register, login, logout) functional +- [x] CRUD operations on entries work +- [x] Search and filter functional +- [x] Entropy algorithm resurfaces entries correctly +- [x] Docker deployment works +- [x] All existing UI preserved +- [x] No localStorage dependencies remain +- [x] Documentation updated + +## Next Steps + +After implementation: + +1. **Deploy to VPS:** + - Set up reverse proxy (nginx) + - Configure HTTPS (Let's Encrypt) + - Set up automated backups + +2. **Enhancements:** + - OAuth providers (Google, GitHub) + - Browser extension + - Export/import functionality + - Tags and categories + - Shared collections + +3. **Monitoring:** + - Error tracking (Sentry) + - Usage analytics + - Performance monitoring From 1cee450a421ba35917da4de9226a80c75b0e9dbe Mon Sep 17 00:00:00 2001 From: Axel Anderson Date: Sat, 6 Dec 2025 14:35:52 -0500 Subject: [PATCH 03/46] feat: add TrailBase backend configuration and schema --- backend/.gitignore | 5 +++++ backend/config.json | 16 ++++++++++++++++ backend/migrations/001_create_entries.sql | 17 +++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 backend/.gitignore create mode 100644 backend/config.json create mode 100644 backend/migrations/001_create_entries.sql diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..ffdb366 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,5 @@ +data/ +*.db +*.db-shm +*.db-wal +trailbase diff --git a/backend/config.json b/backend/config.json new file mode 100644 index 0000000..bf92119 --- /dev/null +++ b/backend/config.json @@ -0,0 +1,16 @@ +{ + "database": { + "path": "data/interne.db" + }, + "server": { + "port": 4000, + "cors": { + "allowed_origins": ["http://localhost:5173"] + } + }, + "auth": { + "jwt_secret": "CHANGE_THIS_IN_PRODUCTION", + "access_token_ttl": 900, + "refresh_token_ttl": 604800 + } +} diff --git a/backend/migrations/001_create_entries.sql b/backend/migrations/001_create_entries.sql new file mode 100644 index 0000000..d5e513f --- /dev/null +++ b/backend/migrations/001_create_entries.sql @@ -0,0 +1,17 @@ +CREATE TABLE entries ( + id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), + user_id TEXT NOT NULL, + url TEXT NOT NULL, + title TEXT NOT NULL, + description TEXT, + duration INTEGER NOT NULL, + interval TEXT NOT NULL CHECK (interval IN ('hours', 'days', 'weeks', 'months', 'years')), + visited INTEGER DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME, + dismissed_at DATETIME, + FOREIGN KEY (user_id) REFERENCES _user(id) ON DELETE CASCADE +); + +CREATE INDEX idx_entries_user_id ON entries(user_id); +CREATE INDEX idx_entries_dismissed_at ON entries(dismissed_at); From 7c6a43b78fdc302b9a46db788b14a75bb7cbaf98 Mon Sep 17 00:00:00 2001 From: Axel Anderson Date: Sat, 6 Dec 2025 14:39:17 -0500 Subject: [PATCH 04/46] feat: add Vite + React + TypeScript frontend foundation --- frontend/.gitignore | 4 + frontend/index.html | 13 + frontend/package.json | 35 + frontend/pnpm-lock.yaml | 2313 +++++++++++++++++++++++++++++++++++ frontend/src/App.tsx | 5 + frontend/src/main.tsx | 9 + frontend/src/vite-env.d.ts | 1 + frontend/tsconfig.json | 21 + frontend/tsconfig.node.json | 13 + frontend/vite.config.ts | 15 + 10 files changed, 2429 insertions(+) create mode 100644 frontend/.gitignore create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/pnpm-lock.yaml create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/vite-env.d.ts create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a4d699a --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +*.local +.DS_Store diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..72be826 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Interne + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..2d7bdaa --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,35 @@ +{ + "name": "interne-frontend", + "private": true, + "version": "0.31.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint . --ext ts,tsx" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "@tanstack/react-query": "^5.59.0", + "dayjs": "^1.11.13", + "lodash.omit": "^4.5.0", + "lodash.orderby": "^4.6.0", + "@react-aria/button": "^3.10.0", + "react-transition-group": "^4.4.5" + }, + "devDependencies": { + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.1", + "@types/lodash.omit": "^4.5.9", + "@types/lodash.orderby": "^4.6.9", + "@types/react-transition-group": "^4.4.11", + "@vitejs/plugin-react": "^4.3.3", + "typescript": "^5.6.3", + "vite": "^5.4.10", + "eslint": "^9.14.0", + "@typescript-eslint/eslint-plugin": "^8.12.2", + "@typescript-eslint/parser": "^8.12.2" + } +} diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100644 index 0000000..7b32b34 --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,2313 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@react-aria/button': + specifier: ^3.10.0 + version: 3.14.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-query': + specifier: ^5.59.0 + version: 5.90.12(react@18.3.1) + dayjs: + specifier: ^1.11.13 + version: 1.11.19 + lodash.omit: + specifier: ^4.5.0 + version: 4.5.0 + lodash.orderby: + specifier: ^4.6.0 + version: 4.6.0 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + react-transition-group: + specifier: ^4.4.5 + version: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + devDependencies: + '@types/lodash.omit': + specifier: ^4.5.9 + version: 4.5.9 + '@types/lodash.orderby': + specifier: ^4.6.9 + version: 4.6.9 + '@types/react': + specifier: ^18.3.11 + version: 18.3.27 + '@types/react-dom': + specifier: ^18.3.1 + version: 18.3.7(@types/react@18.3.27) + '@types/react-transition-group': + specifier: ^4.4.11 + version: 4.4.12(@types/react@18.3.27) + '@typescript-eslint/eslint-plugin': + specifier: ^8.12.2 + version: 8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.12.2 + version: 8.48.1(eslint@9.39.1)(typescript@5.9.3) + '@vitejs/plugin-react': + specifier: ^4.3.3 + version: 4.7.0(vite@5.4.21) + eslint: + specifier: ^9.14.0 + version: 9.39.1 + typescript: + specifier: ^5.6.3 + version: 5.9.3 + vite: + specifier: ^5.4.10 + version: 5.4.21 + +packages: + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.5': + resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.5': + resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.5': + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.1': + resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@formatjs/ecma402-abstract@2.3.6': + resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==} + + '@formatjs/fast-memoize@2.2.7': + resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==} + + '@formatjs/icu-messageformat-parser@2.11.4': + resolution: {integrity: sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==} + + '@formatjs/icu-skeleton-parser@1.8.16': + resolution: {integrity: sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==} + + '@formatjs/intl-localematcher@0.6.2': + resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@internationalized/date@3.10.0': + resolution: {integrity: sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==} + + '@internationalized/message@3.1.8': + resolution: {integrity: sha512-Rwk3j/TlYZhn3HQ6PyXUV0XP9Uv42jqZGNegt0BXlxjE6G3+LwHjbQZAGHhCnCPdaA6Tvd3ma/7QzLlLkJxAWA==} + + '@internationalized/number@3.6.5': + resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==} + + '@internationalized/string@3.2.7': + resolution: {integrity: sha512-D4OHBjrinH+PFZPvfCXvG28n2LSykWcJ7GIioQL+ok0LON15SdfoUssoHzzOUmVZLbRoREsQXVzA6r8JKsbP6A==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@react-aria/button@3.14.2': + resolution: {integrity: sha512-VbLIA+Kd6f/MDjd+TJBUg2+vNDw66pnvsj2E4RLomjI9dfBuN7d+Yo2UnsqKVyhePjCUZ6xxa2yDuD63IOSIYA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/focus@3.21.2': + resolution: {integrity: sha512-JWaCR7wJVggj+ldmM/cb/DXFg47CXR55lznJhZBh4XVqJjMKwaOOqpT5vNN7kpC1wUpXicGNuDnJDN1S/+6dhQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/i18n@3.12.13': + resolution: {integrity: sha512-YTM2BPg0v1RvmP8keHenJBmlx8FXUKsdYIEX7x6QWRd1hKlcDwphfjzvt0InX9wiLiPHsT5EoBTpuUk8SXc0Mg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/interactions@3.25.6': + resolution: {integrity: sha512-5UgwZmohpixwNMVkMvn9K1ceJe6TzlRlAfuYoQDUuOkk62/JVJNDLAPKIf5YMRc7d2B0rmfgaZLMtbREb0Zvkw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/ssr@3.9.10': + resolution: {integrity: sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==} + engines: {node: '>= 12'} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/toolbar@3.0.0-beta.21': + resolution: {integrity: sha512-yRCk/GD8g+BhdDgxd3I0a0c8Ni4Wyo6ERzfSoBkPkwQ4X2E2nkopmraM9D0fXw4UcIr4bnmvADzkHXtBN0XrBg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/utils@3.31.0': + resolution: {integrity: sha512-ABOzCsZrWzf78ysswmguJbx3McQUja7yeGj6/vZo4JVsZNlxAN+E9rs381ExBRI0KzVo6iBTeX5De8eMZPJXig==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-stately/flags@3.1.2': + resolution: {integrity: sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==} + + '@react-stately/toggle@3.9.2': + resolution: {integrity: sha512-dOxs9wrVXHUmA7lc8l+N9NbTJMAaXcYsnNGsMwfXIXQ3rdq+IjWGNYJ52UmNQyRYFcg0jrzRrU16TyGbNjOdNQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-stately/utils@3.10.8': + resolution: {integrity: sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-types/button@3.14.1': + resolution: {integrity: sha512-D8C4IEwKB7zEtiWYVJ3WE/5HDcWlze9mLWQ5hfsBfpePyWCgO3bT/+wjb/7pJvcAocrkXo90QrMm85LcpBtrpg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-types/checkbox@3.10.2': + resolution: {integrity: sha512-ktPkl6ZfIdGS1tIaGSU/2S5Agf2NvXI9qAgtdMDNva0oLyAZ4RLQb6WecPvofw1J7YKXu0VA5Mu7nlX+FM2weQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-types/shared@3.32.1': + resolution: {integrity: sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.53.3': + resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.53.3': + resolution: {integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.53.3': + resolution: {integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.53.3': + resolution: {integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.53.3': + resolution: {integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.53.3': + resolution: {integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.53.3': + resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.53.3': + resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.53.3': + resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.53.3': + resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.53.3': + resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.53.3': + resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.53.3': + resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.53.3': + resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.53.3': + resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.53.3': + resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.53.3': + resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.53.3': + resolution: {integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.53.3': + resolution: {integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.53.3': + resolution: {integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.53.3': + resolution: {integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==} + cpu: [x64] + os: [win32] + + '@swc/helpers@0.5.17': + resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} + + '@tanstack/query-core@5.90.12': + resolution: {integrity: sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==} + + '@tanstack/react-query@5.90.12': + resolution: {integrity: sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==} + peerDependencies: + react: ^18 || ^19 + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/lodash.omit@4.5.9': + resolution: {integrity: sha512-zuAVFLUPJMOzsw6yawshsYGgq2hWUHtsZgeXHZmSFhaQQFC6EQ021uDKHkSjOpNhSvtNSU9165/o3o/Q51GpTw==} + + '@types/lodash.orderby@4.6.9': + resolution: {integrity: sha512-T9o2wkIJOmxXwVTPTmwJ59W6eTi2FseiLR369fxszG649Po/xe9vqFNhf/MtnvT5jrbDiyWKxPFPZbpSVK0SVQ==} + + '@types/lodash@4.17.21': + resolution: {integrity: sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react-transition-group@4.4.12': + resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} + peerDependencies: + '@types/react': '*' + + '@types/react@18.3.27': + resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==} + + '@typescript-eslint/eslint-plugin@8.48.1': + resolution: {integrity: sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.48.1 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.48.1': + resolution: {integrity: sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.48.1': + resolution: {integrity: sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.48.1': + resolution: {integrity: sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.48.1': + resolution: {integrity: sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.48.1': + resolution: {integrity: sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.48.1': + resolution: {integrity: sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.48.1': + resolution: {integrity: sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.48.1': + resolution: {integrity: sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.48.1': + resolution: {integrity: sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + baseline-browser-mapping@2.9.4: + resolution: {integrity: sha512-ZCQ9GEWl73BVm8bu5Fts8nt7MHdbt5vY9bP6WGnUh+r3l8M7CgfyTlwsgCbMC66BNxPr6Xoce3j66Ms5YUQTNA==} + hasBin: true + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001759: + resolution: {integrity: sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + electron-to-chromium@1.5.266: + resolution: {integrity: sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.39.1: + resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + intl-messageformat@10.7.18: + resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.omit@4.5.0: + resolution: {integrity: sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==} + deprecated: This package is deprecated. Use destructuring assignment syntax instead. + + lodash.orderby@4.6.0: + resolution: {integrity: sha512-T0rZxKmghOOf5YPnn8EY5iLYeWCpZq8G41FfqoVHH5QDTAFaghJRmAdLiadEDq+ztgM2q5PjA+Z1fOwGrLgmtg==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + rollup@4.53.3: + resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + update-browserslist-db@1.2.2: + resolution: {integrity: sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.5': {} + + '@babel/core@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.5 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/runtime@7.28.4': {} + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + + '@babel/traverse@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1)': + dependencies: + eslint: 9.39.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.3': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.1': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@formatjs/ecma402-abstract@2.3.6': + dependencies: + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/intl-localematcher': 0.6.2 + decimal.js: 10.6.0 + tslib: 2.8.1 + + '@formatjs/fast-memoize@2.2.7': + dependencies: + tslib: 2.8.1 + + '@formatjs/icu-messageformat-parser@2.11.4': + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + '@formatjs/icu-skeleton-parser': 1.8.16 + tslib: 2.8.1 + + '@formatjs/icu-skeleton-parser@1.8.16': + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + tslib: 2.8.1 + + '@formatjs/intl-localematcher@0.6.2': + dependencies: + tslib: 2.8.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@internationalized/date@3.10.0': + dependencies: + '@swc/helpers': 0.5.17 + + '@internationalized/message@3.1.8': + dependencies: + '@swc/helpers': 0.5.17 + intl-messageformat: 10.7.18 + + '@internationalized/number@3.6.5': + dependencies: + '@swc/helpers': 0.5.17 + + '@internationalized/string@3.2.7': + dependencies: + '@swc/helpers': 0.5.17 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@react-aria/button@3.14.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-aria/interactions': 3.25.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/toolbar': 3.0.0-beta.21(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/utils': 3.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-stately/toggle': 3.9.2(react@18.3.1) + '@react-types/button': 3.14.1(react@18.3.1) + '@react-types/shared': 3.32.1(react@18.3.1) + '@swc/helpers': 0.5.17 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@react-aria/focus@3.21.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-aria/interactions': 3.25.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/utils': 3.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-types/shared': 3.32.1(react@18.3.1) + '@swc/helpers': 0.5.17 + clsx: 2.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@react-aria/i18n@3.12.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@internationalized/date': 3.10.0 + '@internationalized/message': 3.1.8 + '@internationalized/number': 3.6.5 + '@internationalized/string': 3.2.7 + '@react-aria/ssr': 3.9.10(react@18.3.1) + '@react-aria/utils': 3.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-types/shared': 3.32.1(react@18.3.1) + '@swc/helpers': 0.5.17 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@react-aria/interactions@3.25.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-aria/ssr': 3.9.10(react@18.3.1) + '@react-aria/utils': 3.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-stately/flags': 3.1.2 + '@react-types/shared': 3.32.1(react@18.3.1) + '@swc/helpers': 0.5.17 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@react-aria/ssr@3.9.10(react@18.3.1)': + dependencies: + '@swc/helpers': 0.5.17 + react: 18.3.1 + + '@react-aria/toolbar@3.0.0-beta.21(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-aria/focus': 3.21.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/i18n': 3.12.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/utils': 3.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-types/shared': 3.32.1(react@18.3.1) + '@swc/helpers': 0.5.17 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@react-aria/utils@3.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-aria/ssr': 3.9.10(react@18.3.1) + '@react-stately/flags': 3.1.2 + '@react-stately/utils': 3.10.8(react@18.3.1) + '@react-types/shared': 3.32.1(react@18.3.1) + '@swc/helpers': 0.5.17 + clsx: 2.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@react-stately/flags@3.1.2': + dependencies: + '@swc/helpers': 0.5.17 + + '@react-stately/toggle@3.9.2(react@18.3.1)': + dependencies: + '@react-stately/utils': 3.10.8(react@18.3.1) + '@react-types/checkbox': 3.10.2(react@18.3.1) + '@react-types/shared': 3.32.1(react@18.3.1) + '@swc/helpers': 0.5.17 + react: 18.3.1 + + '@react-stately/utils@3.10.8(react@18.3.1)': + dependencies: + '@swc/helpers': 0.5.17 + react: 18.3.1 + + '@react-types/button@3.14.1(react@18.3.1)': + dependencies: + '@react-types/shared': 3.32.1(react@18.3.1) + react: 18.3.1 + + '@react-types/checkbox@3.10.2(react@18.3.1)': + dependencies: + '@react-types/shared': 3.32.1(react@18.3.1) + react: 18.3.1 + + '@react-types/shared@3.32.1(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.53.3': + optional: true + + '@rollup/rollup-android-arm64@4.53.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.53.3': + optional: true + + '@rollup/rollup-darwin-x64@4.53.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.53.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.53.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.53.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.53.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.53.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.53.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.53.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.53.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.53.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.53.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.53.3': + optional: true + + '@swc/helpers@0.5.17': + dependencies: + tslib: 2.8.1 + + '@tanstack/query-core@5.90.12': {} + + '@tanstack/react-query@5.90.12(react@18.3.1)': + dependencies: + '@tanstack/query-core': 5.90.12 + react: 18.3.1 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.5 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.5 + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/lodash.omit@4.5.9': + dependencies: + '@types/lodash': 4.17.21 + + '@types/lodash.orderby@4.6.9': + dependencies: + '@types/lodash': 4.17.21 + + '@types/lodash@4.17.21': {} + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.27)': + dependencies: + '@types/react': 18.3.27 + + '@types/react-transition-group@4.4.12(@types/react@18.3.27)': + dependencies: + '@types/react': 18.3.27 + + '@types/react@18.3.27': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.48.1(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.48.1 + '@typescript-eslint/type-utils': 8.48.1(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.48.1(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.48.1 + eslint: 9.39.1 + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.48.1(eslint@9.39.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.48.1 + '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.48.1 + debug: 4.4.3 + eslint: 9.39.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.48.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.48.1(typescript@5.9.3) + '@typescript-eslint/types': 8.48.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.48.1': + dependencies: + '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/visitor-keys': 8.48.1 + + '@typescript-eslint/tsconfig-utils@8.48.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.48.1(eslint@9.39.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.48.1(eslint@9.39.1)(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.1 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.48.1': {} + + '@typescript-eslint/typescript-estree@8.48.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.48.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.48.1(typescript@5.9.3) + '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/visitor-keys': 8.48.1 + debug: 4.4.3 + minimatch: 9.0.5 + semver: 7.7.3 + tinyglobby: 0.2.15 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.48.1(eslint@9.39.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1) + '@typescript-eslint/scope-manager': 8.48.1 + '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3) + eslint: 9.39.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.48.1': + dependencies: + '@typescript-eslint/types': 8.48.1 + eslint-visitor-keys: 4.2.1 + + '@vitejs/plugin-react@4.7.0(vite@5.4.21)': + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 5.4.21 + transitivePeerDependencies: + - supports-color + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + balanced-match@1.0.2: {} + + baseline-browser-mapping@2.9.4: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.4 + caniuse-lite: 1.0.30001759 + electron-to-chromium: 1.5.266 + node-releases: 2.0.27 + update-browserslist-db: 1.2.2(browserslist@4.28.1) + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001759: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + dayjs@1.11.19: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + deep-is@0.1.4: {} + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.28.4 + csstype: 3.2.3 + + electron-to-chromium@1.5.266: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.39.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.1 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + intl-messageformat@10.7.18: + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/icu-messageformat-parser': 2.11.4 + tslib: 2.8.1 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + lodash.omit@4.5.0: {} + + lodash.orderby@4.6.0: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.27: {} + + object-assign@4.1.1: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + punycode@2.3.1: {} + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-is@16.13.1: {} + + react-refresh@0.17.0: {} + + react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + resolve-from@4.0.0: {} + + rollup@4.53.3: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.53.3 + '@rollup/rollup-android-arm64': 4.53.3 + '@rollup/rollup-darwin-arm64': 4.53.3 + '@rollup/rollup-darwin-x64': 4.53.3 + '@rollup/rollup-freebsd-arm64': 4.53.3 + '@rollup/rollup-freebsd-x64': 4.53.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.53.3 + '@rollup/rollup-linux-arm-musleabihf': 4.53.3 + '@rollup/rollup-linux-arm64-gnu': 4.53.3 + '@rollup/rollup-linux-arm64-musl': 4.53.3 + '@rollup/rollup-linux-loong64-gnu': 4.53.3 + '@rollup/rollup-linux-ppc64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-musl': 4.53.3 + '@rollup/rollup-linux-s390x-gnu': 4.53.3 + '@rollup/rollup-linux-x64-gnu': 4.53.3 + '@rollup/rollup-linux-x64-musl': 4.53.3 + '@rollup/rollup-openharmony-arm64': 4.53.3 + '@rollup/rollup-win32-arm64-msvc': 4.53.3 + '@rollup/rollup-win32-ia32-msvc': 4.53.3 + '@rollup/rollup-win32-x64-gnu': 4.53.3 + '@rollup/rollup-win32-x64-msvc': 4.53.3 + fsevents: 2.3.3 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + semver@6.3.1: {} + + semver@7.7.3: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + source-map-js@1.2.1: {} + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + ts-api-utils@2.1.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript@5.9.3: {} + + update-browserslist-db@1.2.2(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite@5.4.21: + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.53.3 + optionalDependencies: + fsevents: 2.3.3 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..cf472c2 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,5 @@ +function App() { + return
Interne - Coming Soon
+} + +export default App diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..c4c5b6d --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..39a405b --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..1b2185a --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "noEmit": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..59929fe --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:4000', + changeOrigin: true, + }, + }, + }, +}) From 3cee2b7f0cba3fc412f2df7fc920f30e5617e964 Mon Sep 17 00:00:00 2001 From: Axel Anderson Date: Sat, 6 Dec 2025 14:41:46 -0500 Subject: [PATCH 05/46] feat: add TypeScript type definitions --- frontend/src/types/api.ts | 10 ++++++++++ frontend/src/types/entry.ts | 25 +++++++++++++++++++++++++ frontend/src/types/user.ts | 21 +++++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 frontend/src/types/api.ts create mode 100644 frontend/src/types/entry.ts create mode 100644 frontend/src/types/user.ts diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts new file mode 100644 index 0000000..966ae40 --- /dev/null +++ b/frontend/src/types/api.ts @@ -0,0 +1,10 @@ +export interface ApiError { + message: string + code?: string + details?: unknown +} + +export interface ListResponse { + data: T[] + total: number +} diff --git a/frontend/src/types/entry.ts b/frontend/src/types/entry.ts new file mode 100644 index 0000000..d6a9bcf --- /dev/null +++ b/frontend/src/types/entry.ts @@ -0,0 +1,25 @@ +export type Interval = 'hours' | 'days' | 'weeks' | 'months' | 'years' + +export interface Entry { + id: string + user_id: string + url: string + title: string + description: string | null + duration: number + interval: Interval + visited: number + created_at: string + updated_at: string | null + dismissed_at: string | null + // Computed client-side + visible?: boolean + availableAt?: Date +} + +export type CreateEntryInput = Omit< + Entry, + 'id' | 'user_id' | 'created_at' | 'updated_at' | 'visited' | 'visible' | 'availableAt' +> + +export type UpdateEntryInput = Partial diff --git a/frontend/src/types/user.ts b/frontend/src/types/user.ts new file mode 100644 index 0000000..dc68498 --- /dev/null +++ b/frontend/src/types/user.ts @@ -0,0 +1,21 @@ +export interface User { + id: string + email: string + created_at: string +} + +export interface AuthResponse { + user: User + access_token: string + refresh_token: string +} + +export interface LoginCredentials { + email: string + password: string +} + +export interface RegisterCredentials { + email: string + password: string +} From de8df372f0bbe552c9a34717bad7dfa35fd365d6 Mon Sep 17 00:00:00 2001 From: Axel Anderson Date: Sat, 6 Dec 2025 14:44:02 -0500 Subject: [PATCH 06/46] feat: migrate utilities to TypeScript --- frontend/src/utils/constants.ts | 19 +++++++++++++++ frontend/src/utils/date.ts | 12 +++++++++ frontend/src/utils/entropy.ts | 42 ++++++++++++++++++++++++++++++++ frontend/src/utils/formatters.ts | 6 +++++ 4 files changed, 79 insertions(+) create mode 100644 frontend/src/utils/constants.ts create mode 100644 frontend/src/utils/date.ts create mode 100644 frontend/src/utils/entropy.ts create mode 100644 frontend/src/utils/formatters.ts diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts new file mode 100644 index 0000000..a5b4ccc --- /dev/null +++ b/frontend/src/utils/constants.ts @@ -0,0 +1,19 @@ +import type { Interval } from '../types/entry' + +export const INTERVALS: Record, Interval> = { + HOURS: 'hours', + DAYS: 'days', + WEEKS: 'weeks', + MONTHS: 'months', + YEARS: 'years', +} + +export const MODES = { + VIEW: 'VIEW', + EDIT: 'EDIT', +} as const + +export const KEY_CODES = { + ESC: 27, + ENTER: 13, +} as const diff --git a/frontend/src/utils/date.ts b/frontend/src/utils/date.ts new file mode 100644 index 0000000..f504dbd --- /dev/null +++ b/frontend/src/utils/date.ts @@ -0,0 +1,12 @@ +import dayjs, { Dayjs } from 'dayjs' +import relativeTime from 'dayjs/plugin/relativeTime' + +dayjs.extend(relativeTime) + +export const getCurrentDate = (): Dayjs => dayjs() + +export const getDate = (date: string): Dayjs => dayjs(date) + +export const getRelativeTimeFromNow = (date: string): string => { + return dayjs(date).fromNow() +} diff --git a/frontend/src/utils/entropy.ts b/frontend/src/utils/entropy.ts new file mode 100644 index 0000000..e45d865 --- /dev/null +++ b/frontend/src/utils/entropy.ts @@ -0,0 +1,42 @@ +import type { Entry, Interval } from '../types/entry' +import { getCurrentDate, getDate } from './date' + +const MAX = 7 +const MILLIS_IN_DAY = 24 * 60 * 60 * 1000 + +// TODO: make user configurable +const opts = { + entropy: 5, +} + +export const getAvailableAtPlusEntropy = ({ + dismissed_at, + interval, + duration, +}: Pick): { + availableAt: dayjs.Dayjs + diff: number +} => { + const now = getCurrentDate() + const { entropy } = opts + + const availableAt = dismissed_at + ? getDate(dismissed_at).add(duration, interval as any) + : now.subtract(1, 'seconds') + + const diff = availableAt.diff(now) + + if (entropy && diff > MILLIS_IN_DAY) { + const availableAtPlusEntropy = availableAt.add( + Math.floor(Math.random() * ((entropy / 10) * MAX)), + 'days' + ) + + return { + availableAt: availableAtPlusEntropy, + diff: availableAtPlusEntropy.diff(now), + } + } + + return { availableAt, diff } +} diff --git a/frontend/src/utils/formatters.ts b/frontend/src/utils/formatters.ts new file mode 100644 index 0000000..d10bfd1 --- /dev/null +++ b/frontend/src/utils/formatters.ts @@ -0,0 +1,6 @@ +export const toTitleCase = (str: string): string => { + return str + .split(' ') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(' ') +} From a97ed70c093fbd245aa82d58c33985e6cf010db0 Mon Sep 17 00:00:00 2001 From: Axel Anderson Date: Sat, 6 Dec 2025 14:47:52 -0500 Subject: [PATCH 07/46] fix: resolve TypeScript errors and restore original utility behavior --- frontend/src/utils/constants.ts | 4 ++-- frontend/src/utils/entropy.ts | 5 +++-- frontend/src/utils/formatters.ts | 7 +++++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index a5b4ccc..ce573fc 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -9,8 +9,8 @@ export const INTERVALS: Record, Interval> = { } export const MODES = { - VIEW: 'VIEW', - EDIT: 'EDIT', + VIEW: 'view', + EDIT: 'edit', } as const export const KEY_CODES = { diff --git a/frontend/src/utils/entropy.ts b/frontend/src/utils/entropy.ts index e45d865..d13d324 100644 --- a/frontend/src/utils/entropy.ts +++ b/frontend/src/utils/entropy.ts @@ -1,4 +1,5 @@ -import type { Entry, Interval } from '../types/entry' +import type { Entry } from '../types/entry' +import type { Dayjs } from 'dayjs' import { getCurrentDate, getDate } from './date' const MAX = 7 @@ -14,7 +15,7 @@ export const getAvailableAtPlusEntropy = ({ interval, duration, }: Pick): { - availableAt: dayjs.Dayjs + availableAt: Dayjs diff: number } => { const now = getCurrentDate() diff --git a/frontend/src/utils/formatters.ts b/frontend/src/utils/formatters.ts index d10bfd1..cf9603f 100644 --- a/frontend/src/utils/formatters.ts +++ b/frontend/src/utils/formatters.ts @@ -1,6 +1,9 @@ export const toTitleCase = (str: string): string => { return str - .split(' ') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .split('_') .join(' ') + .replace( + /\w\S*/g, + (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() + ) } From d8cc2351fed92dbd4ddab46604175b3cd9a4d44e Mon Sep 17 00:00:00 2001 From: Axel Anderson Date: Sat, 6 Dec 2025 14:50:38 -0500 Subject: [PATCH 08/46] feat: copy CSS modules to frontend --- frontend/src/styles/Footer.module.css | 17 ++++ frontend/src/styles/Forms.module.css | 87 ++++++++++++++++ frontend/src/styles/Header.module.css | 56 ++++++++++ frontend/src/styles/Index.module.css | 141 ++++++++++++++++++++++++++ frontend/src/styles/Pages.module.css | 19 ++++ frontend/src/styles/globals.css | 36 +++++++ 6 files changed, 356 insertions(+) create mode 100644 frontend/src/styles/Footer.module.css create mode 100644 frontend/src/styles/Forms.module.css create mode 100644 frontend/src/styles/Header.module.css create mode 100644 frontend/src/styles/Index.module.css create mode 100644 frontend/src/styles/Pages.module.css create mode 100644 frontend/src/styles/globals.css diff --git a/frontend/src/styles/Footer.module.css b/frontend/src/styles/Footer.module.css new file mode 100644 index 0000000..a9cdf32 --- /dev/null +++ b/frontend/src/styles/Footer.module.css @@ -0,0 +1,17 @@ +.footer { + width: 100%; + height: 100px; + border-top: 1px solid #ddd; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + font-family: athelas, georgia, serif; + font-style: italic; + line-height: 1.5; + color: rgba(0, 0, 0, 0.3); +} + +.footer a:hover { + color: rgba(0, 0, 0, 0.8); +} diff --git a/frontend/src/styles/Forms.module.css b/frontend/src/styles/Forms.module.css new file mode 100644 index 0000000..6418d02 --- /dev/null +++ b/frontend/src/styles/Forms.module.css @@ -0,0 +1,87 @@ +.container, +.button-container { + margin-bottom: 1rem; +} + +.button-container { + display: flex; + justify-content: center; +} + +.form { + width: 100%; + max-width: 800px; + margin-bottom: 2rem; +} + +.label { + display: block; + margin-bottom: 0.5rem; + font-family: athelas, georgia, serif; + font-size: 1.25rem; + font-style: italic; +} + +.input, +.textarea { + font-size: 1.25rem; + border: 1px solid #ddd; + border-radius: 0.125rem; + display: block; + width: 100%; + padding: 0.25rem 0.5rem; + -webkit-appearance: none; + -moz-appearance: none; +} + +.textarea { + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, + monospace; + min-height: 80vh; +} + +.select { + font-size: 1.25rem; + border: 1px solid #ddd; + border-radius: 0.125rem; + display: block; + width: 100%; + padding: 0.25rem 0.5rem; + /* -webkit-appearance: none; */ + /* -moz-appearance: none; */ +} + +.button { + font-size: 1rem; + font-weight: 700; + background: #fff; + border: 1px solid #ddd; + border-radius: 0.5rem; + display: flex; + justify-content: center; + padding: 0.5rem 1.5rem; + cursor: pointer; + transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease; +} +.button:hover, +.button:focus { + background: #f4f4f4; +} + +.button:active { + background: #eee; +} + +.input::-moz-focus-inner, +.button::-moz-focus-inner, +.textarea::-moz-focus-inner { + border: 0; + padding: 0; +} + +.error { + display: flex; + justify-content: center; + margin-bottom: 1rem; + color: #e7040f; +} diff --git a/frontend/src/styles/Header.module.css b/frontend/src/styles/Header.module.css new file mode 100644 index 0000000..3dea007 --- /dev/null +++ b/frontend/src/styles/Header.module.css @@ -0,0 +1,56 @@ +.header { + position: fixed; + top: 1rem; + left: 0; + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + padding: 0 1rem; + z-index: 100; + + /* TODO use background? */ + /* background: #fff; */ + /* top: 0; */ + /* padding: 0.5rem; */ +} + +.title, +.date { + font-family: athelas, georgia, serif; + font-size: 1.25rem; + line-height: 1.5; + width: 150px; + font-style: italic; +} + +.title { + margin: 0; + font-weight: 400; +} + +.date { + text-align: right; + height: 30px; + position: relative; +} + +.date div { + position: absolute; + top: 0; + right: 0; + min-width: 150px; +} + +.mode { + font-weight: 700; + font-size: 1rem; + line-height: 1.5; + cursor: pointer; +} + +@media (max-width: 600px) { + .date { + display: none; + } +} diff --git a/frontend/src/styles/Index.module.css b/frontend/src/styles/Index.module.css new file mode 100644 index 0000000..634f768 --- /dev/null +++ b/frontend/src/styles/Index.module.css @@ -0,0 +1,141 @@ +.filter { + line-height: 1.5; + font-size: 1rem; + cursor: pointer; + position: fixed; + top: 50%; + transform: rotate(270deg); + margin: -12px 0 0 0.5rem; +} + +.filter span { + font-weight: 700; +} + +.grid { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + width: 100%; + max-width: 800px; +} + +.card { + margin: 0 1rem 2rem; + flex-basis: 45%; + max-width: 45%; + padding: 1.5rem; + text-align: left; + color: inherit; + text-decoration: none; + background: #fff; + border: 1px solid #ddd; + border-radius: 0.5rem; + transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease; +} + +.card:hover, +.card:focus { + background: #f4f4f4; +} + +.card:active { + background: #eee; +} + +.unavailable { + opacity: 30%; +} + +.unavailable:hover, +.unavailable:focus { + background: #fff; + opacity: 70%; +} + +.card:active { + opacity: 60%; +} + +.card h2 { + margin: 0 0 0.5rem; + font-size: 1.5rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.card p { + height: 30px; + margin: 0 0 0.5rem; + font-size: 1.25rem; + line-height: 1.5; + font-family: athelas, georgia, serif; + font-style: italic; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.viewed { + font-size: 0.875rem; + margin-bottom: 0.5rem; +} + +.flex-between { + display: flex; + justify-content: space-between; +} + +.title { + display: flex; + font-size: 1.5rem; +} + +.rarr { + margin: 0 0 0.5rem 0.25rem; +} + +.availability { + font-size: 0.875rem; +} + +.controls { + font-size: 0.875rem; + display: flex; +} + +.ignore, +.edit, +.delete { + cursor: pointer; +} + +.ignore, +.edit { + margin-right: 0.5rem; +} + +.empty { + font-size: 1.25rem; + font-style: italic; +} + +@media (max-width: 1000px) { + .card { + flex-basis: 40%; + max-width: 40%; + } +} + +@media (max-width: 600px) { + .grid { + flex-direction: column; + } + + .card { + width: 80%; + max-width: 80%; + } +} diff --git a/frontend/src/styles/Pages.module.css b/frontend/src/styles/Pages.module.css new file mode 100644 index 0000000..ccdaf83 --- /dev/null +++ b/frontend/src/styles/Pages.module.css @@ -0,0 +1,19 @@ +.container { + background: #fafafa; + padding: 0 1rem; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.main { + min-height: 100vh; + width: 100%; + padding: 4rem 0; + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css new file mode 100644 index 0000000..f20313b --- /dev/null +++ b/frontend/src/styles/globals.css @@ -0,0 +1,36 @@ +html, +body { + padding: 0; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'helvetica neue', helvetica, + arial, sans-serif; + color: rgba(0, 0, 0, 0.8); +} + +a { + color: inherit; + text-decoration: none; +} + +* { + box-sizing: border-box; +} + +.fade-enter { + opacity: 0; +} + +.fade-enter-active { + opacity: 1; + transform: translateX(0); + transition: opacity 200ms, transform 200ms; +} + +.fade-exit { + opacity: 1; +} + +.fade-exit-active { + opacity: 0; + transition: opacity 200ms, transform 200ms; +} From 05fadcb8b5164c4b3ea19d9bd984be152ef78fbb Mon Sep 17 00:00:00 2001 From: Axel Anderson Date: Sat, 6 Dec 2025 14:52:44 -0500 Subject: [PATCH 09/46] feat: add TrailBase API client and services --- frontend/src/services/auth.ts | 38 +++++++++++++++++++++ frontend/src/services/entries.ts | 28 ++++++++++++++++ frontend/src/services/trailbase.ts | 53 ++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 frontend/src/services/auth.ts create mode 100644 frontend/src/services/entries.ts create mode 100644 frontend/src/services/trailbase.ts diff --git a/frontend/src/services/auth.ts b/frontend/src/services/auth.ts new file mode 100644 index 0000000..1228b81 --- /dev/null +++ b/frontend/src/services/auth.ts @@ -0,0 +1,38 @@ +import type { AuthResponse, LoginCredentials, RegisterCredentials, User } from '../types/user' +import { trailbase } from './trailbase' + +export async function login(credentials: LoginCredentials): Promise { + const response = await trailbase.request('/auth/login', { + method: 'POST', + body: JSON.stringify(credentials), + }) + + trailbase.setAccessToken(response.access_token) + return response +} + +export async function register(credentials: RegisterCredentials): Promise { + const response = await trailbase.request('/auth/register', { + method: 'POST', + body: JSON.stringify(credentials), + }) + + trailbase.setAccessToken(response.access_token) + return response +} + +export async function logout(): Promise { + trailbase.clearAccessToken() +} + +export async function getCurrentUser(): Promise { + const token = trailbase.getAccessToken() + if (!token) return null + + try { + return await trailbase.request('/auth/me') + } catch { + trailbase.clearAccessToken() + return null + } +} diff --git a/frontend/src/services/entries.ts b/frontend/src/services/entries.ts new file mode 100644 index 0000000..36b67fc --- /dev/null +++ b/frontend/src/services/entries.ts @@ -0,0 +1,28 @@ +import type { Entry, CreateEntryInput, UpdateEntryInput } from '../types/entry' +import type { ListResponse } from '../types/api' +import { trailbase } from './trailbase' + +export async function fetchEntries(): Promise { + const response = await trailbase.request>('/records/v1/entries') + return response.data +} + +export async function createEntry(input: CreateEntryInput): Promise { + return trailbase.request('/records/v1/entries', { + method: 'POST', + body: JSON.stringify(input), + }) +} + +export async function updateEntry(id: string, updates: UpdateEntryInput): Promise { + return trailbase.request(`/records/v1/entries/${id}`, { + method: 'PATCH', + body: JSON.stringify(updates), + }) +} + +export async function deleteEntry(id: string): Promise { + await trailbase.request(`/records/v1/entries/${id}`, { + method: 'DELETE', + }) +} diff --git a/frontend/src/services/trailbase.ts b/frontend/src/services/trailbase.ts new file mode 100644 index 0000000..c6e8a96 --- /dev/null +++ b/frontend/src/services/trailbase.ts @@ -0,0 +1,53 @@ +import type { Entry } from '../types/entry' +import type { AuthResponse, LoginCredentials, RegisterCredentials } from '../types/user' + +const API_BASE = '/api' + +class TrailBaseClient { + private accessToken: string | null = null + + async request( + endpoint: string, + options: RequestInit = {} + ): Promise { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + ...options.headers, + } + + if (this.accessToken) { + headers['Authorization'] = `Bearer ${this.accessToken}` + } + + const response = await fetch(`${API_BASE}${endpoint}`, { + ...options, + headers, + }) + + if (!response.ok) { + const error = await response.json().catch(() => ({ message: response.statusText })) + throw new Error(error.message || 'Request failed') + } + + return response.json() + } + + setAccessToken(token: string) { + this.accessToken = token + localStorage.setItem('access_token', token) + } + + clearAccessToken() { + this.accessToken = null + localStorage.removeItem('access_token') + } + + getAccessToken(): string | null { + if (!this.accessToken) { + this.accessToken = localStorage.getItem('access_token') + } + return this.accessToken + } +} + +export const trailbase = new TrailBaseClient() From 640bbe23029d7ccd25fe2447b4b3effc9d596f0f Mon Sep 17 00:00:00 2001 From: Axel Anderson Date: Sat, 6 Dec 2025 14:55:13 -0500 Subject: [PATCH 10/46] feat: add React Query hooks for auth and entries --- frontend/src/hooks/useAuth.ts | 45 ++++++++++++++++++++++++++++++++ frontend/src/hooks/useEntries.ts | 44 +++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 frontend/src/hooks/useAuth.ts create mode 100644 frontend/src/hooks/useEntries.ts diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts new file mode 100644 index 0000000..1c43124 --- /dev/null +++ b/frontend/src/hooks/useAuth.ts @@ -0,0 +1,45 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import * as authService from '../services/auth' +import type { LoginCredentials, RegisterCredentials } from '../types/user' + +export function useCurrentUser() { + return useQuery({ + queryKey: ['user'], + queryFn: authService.getCurrentUser, + staleTime: 1000 * 60 * 5, // 5 minutes + }) +} + +export function useLogin() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (credentials: LoginCredentials) => authService.login(credentials), + onSuccess: (data) => { + queryClient.setQueryData(['user'], data.user) + }, + }) +} + +export function useRegister() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (credentials: RegisterCredentials) => authService.register(credentials), + onSuccess: (data) => { + queryClient.setQueryData(['user'], data.user) + }, + }) +} + +export function useLogout() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: authService.logout, + onSuccess: () => { + queryClient.setQueryData(['user'], null) + queryClient.clear() + }, + }) +} diff --git a/frontend/src/hooks/useEntries.ts b/frontend/src/hooks/useEntries.ts new file mode 100644 index 0000000..1289aff --- /dev/null +++ b/frontend/src/hooks/useEntries.ts @@ -0,0 +1,44 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import * as entriesService from '../services/entries' +import type { CreateEntryInput, UpdateEntryInput } from '../types/entry' + +export function useEntries() { + return useQuery({ + queryKey: ['entries'], + queryFn: entriesService.fetchEntries, + }) +} + +export function useCreateEntry() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (input: CreateEntryInput) => entriesService.createEntry(input), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['entries'] }) + }, + }) +} + +export function useUpdateEntry() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: ({ id, updates }: { id: string; updates: UpdateEntryInput }) => + entriesService.updateEntry(id, updates), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['entries'] }) + }, + }) +} + +export function useDeleteEntry() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (id: string) => entriesService.deleteEntry(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['entries'] }) + }, + }) +} From 255a71391bc10fd984605c590505d5a7f7c81ac2 Mon Sep 17 00:00:00 2001 From: Axel Anderson Date: Sat, 6 Dec 2025 14:57:21 -0500 Subject: [PATCH 11/46] feat: migrate Forms component to TypeScript --- frontend/src/components/Forms.tsx | 87 +++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 frontend/src/components/Forms.tsx diff --git a/frontend/src/components/Forms.tsx b/frontend/src/components/Forms.tsx new file mode 100644 index 0000000..99a625d --- /dev/null +++ b/frontend/src/components/Forms.tsx @@ -0,0 +1,87 @@ +import React, { forwardRef } from 'react' +import { useButton } from '@react-aria/button' +import styles from '../styles/Forms.module.css' + +interface FormProps { + children: React.ReactNode +} + +export function Form({ children }: FormProps) { + return
{children}
+} + +interface InputProps { + type?: string + value: string | number + label: string + placeholder?: string + onChange: (value: string) => void + pattern?: string + min?: number +} + +export const Input = forwardRef( + ({ type = 'text', value, label, placeholder, onChange, pattern, min }, ref) => { + return ( +
+ + onChange(e.target.value)} + pattern={pattern} + min={min} + /> +
+ ) + } +) + +Input.displayName = 'Input' + +interface SelectOption { + id: string + display: string +} + +interface SelectProps { + label: string + value: string + onChange: (value: string) => void + options: SelectOption[] +} + +export function Select({ label, value, onChange, options }: SelectProps) { + return ( +
+ + +
+ ) +} + +interface ButtonProps { + label: string + onClick: () => void + children: React.ReactNode +} + +export function Button({ label, onClick, children }: ButtonProps) { + const ref = React.useRef(null) + const { buttonProps } = useButton({ onPress: onClick }, ref) + + return ( + + ) +} From 94f623f89e856160da0e81f91ba7ebc1fb7a2bf0 Mon Sep 17 00:00:00 2001 From: Axel Anderson Date: Sat, 6 Dec 2025 15:00:09 -0500 Subject: [PATCH 12/46] fix: restore original Forms component functionality --- frontend/src/components/Forms.tsx | 68 +++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/Forms.tsx b/frontend/src/components/Forms.tsx index 99a625d..8afc85b 100644 --- a/frontend/src/components/Forms.tsx +++ b/frontend/src/components/Forms.tsx @@ -1,4 +1,4 @@ -import React, { forwardRef } from 'react' +import React, { forwardRef, useRef, useImperativeHandle } from 'react' import { useButton } from '@react-aria/button' import styles from '../styles/Forms.module.css' @@ -13,20 +13,35 @@ export function Form({ children }: FormProps) { interface InputProps { type?: string value: string | number - label: string + label?: string placeholder?: string onChange: (value: string) => void pattern?: string min?: number } -export const Input = forwardRef( - ({ type = 'text', value, label, placeholder, onChange, pattern, min }, ref) => { +export interface InputRef { + focus: () => void + addEventListener: (type: string, listener: EventListener) => void + className: string +} + +export const Input = forwardRef( + ({ type = 'text', value, label, placeholder, onChange, pattern, min, ...props }, ref) => { + const inputRef = useRef(null) + + useImperativeHandle(ref, () => ({ + focus: () => inputRef.current?.focus(), + addEventListener: (type: string, listener: EventListener) => + inputRef.current?.addEventListener(type, listener), + className: inputRef.current?.className || '', + })) + return ( -
- +
+ {!!label && } ( onChange={(e) => onChange(e.target.value)} pattern={pattern} min={min} + {...props} />
) @@ -48,7 +64,7 @@ interface SelectOption { } interface SelectProps { - label: string + label?: string value: string onChange: (value: string) => void options: SelectOption[] @@ -56,8 +72,8 @@ interface SelectProps { export function Select({ label, value, onChange, options }: SelectProps) { return ( -
- +
+ {!!label && }