From 7884ebbfd48a9727fc9bdd6a31ad15e34b1e4f92 Mon Sep 17 00:00:00 2001 From: Trey t Date: Tue, 3 Mar 2026 11:37:41 -0600 Subject: [PATCH] =?UTF-8?q?feat:=20Phase=204-5=20=E2=80=94=20demo=20mode,?= =?UTF-8?q?=20polish,=20deploy,=20and=20bug=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add demo mode with mock data provider, Docker deployment, Playwright tests, PostHog analytics, error boundaries, and SEO metadata. Fix residences API response unwrapping, kanban drag-and-drop with optimistic updates, trailing slash proxy redirects, and column name mismatches with Go API. Co-Authored-By: Claude Opus 4.6 --- .dockerignore | 10 + .gitignore | 3 + CHECKS | 3 + Dockerfile | 34 + next.config.ts | 17 +- package-lock.json | 658 +++++++++++++++++- package.json | 9 +- playwright.config.ts | 34 + src/app/(auth)/forgot-password/page.tsx | 5 +- src/app/(auth)/login/page.tsx | 8 +- src/app/(auth)/register/page.tsx | 20 +- src/app/api/auth/login/route.ts | 2 +- src/app/api/auth/logout/route.ts | 2 +- src/app/api/auth/me/route.ts | 2 +- src/app/api/proxy/[...path]/route.ts | 4 +- src/app/app/contractors/[id]/edit/page.tsx | 11 +- src/app/app/contractors/[id]/page.tsx | 9 +- src/app/app/contractors/new/page.tsx | 11 +- src/app/app/contractors/page.tsx | 12 +- src/app/app/documents/[id]/edit/page.tsx | 11 +- src/app/app/documents/[id]/page.tsx | 11 +- src/app/app/documents/new/page.tsx | 11 +- src/app/app/documents/page.tsx | 14 +- src/app/app/layout.tsx | 30 +- src/app/app/page.tsx | 24 +- src/app/app/residences/[id]/edit/page.tsx | 11 +- src/app/app/residences/[id]/page.tsx | 24 +- src/app/app/residences/[id]/share/page.tsx | 4 +- src/app/app/residences/join/page.tsx | 15 +- src/app/app/residences/new/page.tsx | 11 +- src/app/app/residences/page.tsx | 10 +- src/app/app/settings/layout.tsx | 15 +- src/app/app/settings/page.tsx | 15 +- src/app/app/tasks/[id]/complete/page.tsx | 11 +- src/app/app/tasks/[id]/edit/page.tsx | 13 +- src/app/app/tasks/[id]/page.tsx | 5 +- src/app/app/tasks/new/page.tsx | 13 +- src/app/app/tasks/page.tsx | 15 +- .../demo/app/contractors/[id]/edit/page.tsx | 2 + src/app/demo/app/contractors/[id]/page.tsx | 2 + src/app/demo/app/contractors/new/page.tsx | 2 + src/app/demo/app/contractors/page.tsx | 2 + src/app/demo/app/documents/[id]/edit/page.tsx | 2 + src/app/demo/app/documents/[id]/page.tsx | 2 + src/app/demo/app/documents/new/page.tsx | 2 + src/app/demo/app/documents/page.tsx | 2 + src/app/demo/app/layout.tsx | 32 + src/app/demo/app/page.tsx | 2 + .../demo/app/residences/[id]/edit/page.tsx | 2 + src/app/demo/app/residences/[id]/page.tsx | 2 + .../demo/app/residences/[id]/share/page.tsx | 2 + src/app/demo/app/residences/join/page.tsx | 2 + src/app/demo/app/residences/new/page.tsx | 2 + src/app/demo/app/residences/page.tsx | 2 + src/app/demo/app/settings/layout.tsx | 2 + .../demo/app/settings/notifications/page.tsx | 2 + src/app/demo/app/settings/page.tsx | 2 + src/app/demo/app/settings/profile/page.tsx | 2 + .../demo/app/settings/subscription/page.tsx | 2 + src/app/demo/app/tasks/[id]/complete/page.tsx | 2 + src/app/demo/app/tasks/[id]/edit/page.tsx | 2 + src/app/demo/app/tasks/[id]/page.tsx | 2 + src/app/demo/app/tasks/new/page.tsx | 2 + src/app/demo/app/tasks/page.tsx | 2 + src/app/demo/layout.tsx | 21 + src/app/demo/page.tsx | 41 ++ src/app/error.tsx | 27 + src/app/layout.tsx | 36 +- src/app/not-found.tsx | 20 + .../contractors/contractor-card.tsx | 14 +- .../contractors/contractor-form.tsx | 2 +- src/components/dashboard/recent-activity.tsx | 4 +- src/components/dashboard/stats-cards.tsx | 4 +- src/components/demo/demo-banner.tsx | 32 + src/components/documents/document-card.tsx | 8 +- src/components/documents/document-form.tsx | 2 +- src/components/documents/image-gallery.tsx | 11 +- src/components/layout/mobile-nav.tsx | 21 +- src/components/layout/nav-items.ts | 21 +- src/components/layout/sidebar.tsx | 17 +- src/components/layout/top-bar.tsx | 14 +- .../notifications/notification-bell.tsx | 6 +- src/components/onboarding/complete-step.tsx | 6 +- src/components/residences/residence-card.tsx | 9 +- .../settings/change-password-form.tsx | 3 + .../settings/delete-account-section.tsx | 2 + .../settings/notification-preferences.tsx | 10 +- src/components/settings/profile-form.tsx | 3 + src/components/shared/error-banner.tsx | 4 +- src/components/shared/form-field.tsx | 5 +- src/components/sharing/share-code-display.tsx | 11 +- src/components/sharing/user-management.tsx | 5 + src/components/tasks/kanban-board.tsx | 79 ++- src/components/tasks/kanban-column.tsx | 94 ++- src/components/tasks/task-actions-menu.tsx | 42 +- src/components/tasks/task-card.tsx | 10 +- src/components/tasks/task-form.tsx | 4 +- src/components/ui/dialog.tsx | 2 +- src/components/ui/sonner.tsx | 25 + src/lib/analytics.ts | 47 ++ src/lib/analytics/posthog-provider.tsx | 22 + src/lib/api/auth.ts | 18 +- src/lib/api/client.ts | 8 +- src/lib/api/residences.ts | 19 +- src/lib/demo/data-provider-context.tsx | 28 + src/lib/demo/data-provider.ts | 173 +++++ src/lib/demo/demo-provider.ts | 358 ++++++++++ src/lib/demo/demo-store.ts | 489 +++++++++++++ src/lib/demo/mock-data/contractors.ts | 109 +++ src/lib/demo/mock-data/documents.ts | 100 +++ src/lib/demo/mock-data/index.ts | 7 + src/lib/demo/mock-data/lookups.ts | 72 ++ src/lib/demo/mock-data/notifications.ts | 104 +++ src/lib/demo/mock-data/residences.ts | 65 ++ src/lib/demo/mock-data/tasks.ts | 280 ++++++++ src/lib/demo/mock-data/user.ts | 11 + src/lib/demo/real-provider.ts | 99 +++ src/lib/hooks/use-auth.ts | 15 +- src/lib/hooks/use-contractors.ts | 25 +- src/lib/hooks/use-documents.ts | 30 +- src/lib/hooks/use-lookups.ts | 20 +- src/lib/hooks/use-notifications.ts | 20 +- src/lib/hooks/use-residences.ts | 18 +- src/lib/hooks/use-sharing.ts | 17 +- src/lib/hooks/use-subscription.ts | 11 +- src/lib/hooks/use-tasks.ts | 40 +- tests/auth.spec.ts | 31 + tests/contractors.spec.ts | 16 + tests/demo.spec.ts | 40 ++ tests/documents.spec.ts | 16 + tests/residences.spec.ts | 21 + tests/responsive.spec.ts | 40 ++ tests/tasks.spec.ts | 21 + 133 files changed, 3904 insertions(+), 300 deletions(-) create mode 100644 .dockerignore create mode 100644 CHECKS create mode 100644 Dockerfile create mode 100644 playwright.config.ts create mode 100644 src/app/demo/app/contractors/[id]/edit/page.tsx create mode 100644 src/app/demo/app/contractors/[id]/page.tsx create mode 100644 src/app/demo/app/contractors/new/page.tsx create mode 100644 src/app/demo/app/contractors/page.tsx create mode 100644 src/app/demo/app/documents/[id]/edit/page.tsx create mode 100644 src/app/demo/app/documents/[id]/page.tsx create mode 100644 src/app/demo/app/documents/new/page.tsx create mode 100644 src/app/demo/app/documents/page.tsx create mode 100644 src/app/demo/app/layout.tsx create mode 100644 src/app/demo/app/page.tsx create mode 100644 src/app/demo/app/residences/[id]/edit/page.tsx create mode 100644 src/app/demo/app/residences/[id]/page.tsx create mode 100644 src/app/demo/app/residences/[id]/share/page.tsx create mode 100644 src/app/demo/app/residences/join/page.tsx create mode 100644 src/app/demo/app/residences/new/page.tsx create mode 100644 src/app/demo/app/residences/page.tsx create mode 100644 src/app/demo/app/settings/layout.tsx create mode 100644 src/app/demo/app/settings/notifications/page.tsx create mode 100644 src/app/demo/app/settings/page.tsx create mode 100644 src/app/demo/app/settings/profile/page.tsx create mode 100644 src/app/demo/app/settings/subscription/page.tsx create mode 100644 src/app/demo/app/tasks/[id]/complete/page.tsx create mode 100644 src/app/demo/app/tasks/[id]/edit/page.tsx create mode 100644 src/app/demo/app/tasks/[id]/page.tsx create mode 100644 src/app/demo/app/tasks/new/page.tsx create mode 100644 src/app/demo/app/tasks/page.tsx create mode 100644 src/app/demo/layout.tsx create mode 100644 src/app/demo/page.tsx create mode 100644 src/app/error.tsx create mode 100644 src/app/not-found.tsx create mode 100644 src/components/demo/demo-banner.tsx create mode 100644 src/components/ui/sonner.tsx create mode 100644 src/lib/analytics.ts create mode 100644 src/lib/analytics/posthog-provider.tsx create mode 100644 src/lib/demo/data-provider-context.tsx create mode 100644 src/lib/demo/data-provider.ts create mode 100644 src/lib/demo/demo-provider.ts create mode 100644 src/lib/demo/demo-store.ts create mode 100644 src/lib/demo/mock-data/contractors.ts create mode 100644 src/lib/demo/mock-data/documents.ts create mode 100644 src/lib/demo/mock-data/index.ts create mode 100644 src/lib/demo/mock-data/lookups.ts create mode 100644 src/lib/demo/mock-data/notifications.ts create mode 100644 src/lib/demo/mock-data/residences.ts create mode 100644 src/lib/demo/mock-data/tasks.ts create mode 100644 src/lib/demo/mock-data/user.ts create mode 100644 src/lib/demo/real-provider.ts create mode 100644 tests/auth.spec.ts create mode 100644 tests/contractors.spec.ts create mode 100644 tests/demo.spec.ts create mode 100644 tests/documents.spec.ts create mode 100644 tests/residences.spec.ts create mode 100644 tests/responsive.spec.ts create mode 100644 tests/tasks.spec.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1166050 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +node_modules +.next +.git +.gitignore +*.md +.env* +.claude +tests +playwright-report +test-results diff --git a/.gitignore b/.gitignore index 5ef6a52..6bd0845 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# claude +.claude/ diff --git a/CHECKS b/CHECKS new file mode 100644 index 0000000..cb04103 --- /dev/null +++ b/CHECKS @@ -0,0 +1,3 @@ +WAIT=10 +ATTEMPTS=6 +/ Casera diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5c333c3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +# Build stage +FROM node:20-alpine AS builder + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . +RUN npm run build + +# Production stage +FROM node:20-alpine AS production + +WORKDIR /app + +# Create non-root user +RUN addgroup -g 1001 nodejs && adduser -u 1001 -G nodejs -s /bin/sh -D nextjs + +# Copy standalone build +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +COPY --from=builder --chown=nextjs:nodejs /app/public ./public + +USER nextjs + +EXPOSE 3000 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ + CMD curl -f http://localhost:3000/ || exit 1 + +CMD ["node", "server.js"] diff --git a/next.config.ts b/next.config.ts index e9ffa30..43f44ec 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,20 @@ import type { NextConfig } from "next"; +import bundleAnalyzer from "@next/bundle-analyzer"; + +const withBundleAnalyzer = bundleAnalyzer({ + enabled: process.env.ANALYZE === "true", +}); const nextConfig: NextConfig = { - /* config options here */ + output: "standalone", + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "casera.treytartt.com", + }, + ], + }, }; -export default nextConfig; +export default withBundleAnalyzer(nextConfig); diff --git a/package-lock.json b/package-lock.json index f03b4b0..ce9bb0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,17 +19,21 @@ "date-fns": "^4.1.0", "lucide-react": "^0.576.0", "next": "16.1.6", + "posthog-js": "^1.357.2", "radix-ui": "^1.4.3", "react": "19.2.3", "react-day-picker": "^9.14.0", "react-dom": "19.2.3", "react-hook-form": "^7.71.2", "recharts": "^3.7.0", + "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "zod": "^4.3.6", "zustand": "^5.0.11" }, "devDependencies": { + "@axe-core/playwright": "^4.11.1", + "@next/bundle-analyzer": "^16.1.6", "@playwright/test": "^1.58.2", "@tailwindcss/postcss": "^4", "@types/node": "^20", @@ -79,6 +83,19 @@ "nup": "bin/nup.mjs" } }, + "node_modules/@axe-core/playwright": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.1.tgz", + "integrity": "sha512-mKEfoUIB1MkVTht0BGZFXtSAEKXMJoDkyV5YZ9jbBmZCcWDz71tegNsdTkIN8zc/yMi5Gm2kx7Z5YQ9PfWNAWw==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "axe-core": "~4.11.1" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -518,6 +535,16 @@ "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==", "license": "MIT" }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@dnd-kit/accessibility": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", @@ -2217,6 +2244,16 @@ "@tybys/wasm-util": "^0.10.0" } }, + "node_modules/@next/bundle-analyzer": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-16.1.6.tgz", + "integrity": "sha512-ee2kagdTaeEWPlotgdTOqFHYcD3e2m2bbE3I9Rq2i6ABYi5OgopmtEUe8NM23viaYxLV2tDH/2nd5+qKoEr6cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "webpack-bundle-analyzer": "4.10.1" + } + }, "node_modules/@next/env": { "version": "16.1.6", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz", @@ -2476,6 +2513,252 @@ "dev": true, "license": "MIT" }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz", + "integrity": "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.208.0.tgz", + "integrity": "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/otlp-exporter-base": "0.208.0", + "@opentelemetry/otlp-transformer": "0.208.0", + "@opentelemetry/sdk-logs": "0.208.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.208.0.tgz", + "integrity": "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/otlp-transformer": "0.208.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.208.0.tgz", + "integrity": "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/sdk-logs": "0.208.0", + "@opentelemetry/sdk-metrics": "2.2.0", + "@opentelemetry/sdk-trace-base": "2.2.0", + "protobufjs": "^7.3.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.1.tgz", + "integrity": "sha512-BViBCdE/GuXRlp9k7nS1w6wJvY5fnFX5XvuEtWsTAOQFIO89Eru7lGW3WbfbxtCuZ/GbrJfAziXG0w0dpxL7eQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.1.tgz", + "integrity": "sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.208.0.tgz", + "integrity": "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", + "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", + "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@playwright/test": { "version": "1.58.2", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", @@ -2492,6 +2775,92 @@ "node": ">=18" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@posthog/core": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.23.2.tgz", + "integrity": "sha512-zTDdda9NuSHrnwSOfFMxX/pyXiycF4jtU1kTr8DL61dHhV+7LF6XF1ndRZZTuaGGbfbb/GJYkEsjEX9SXfNZeQ==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.6" + } + }, + "node_modules/@posthog/types": { + "version": "1.357.2", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.357.2.tgz", + "integrity": "sha512-1I78qKgAl78DNesAAe5HjmOuUc9MJKhClbErKHhwAi9rKualtPaM1fHMBqbUsBaeQRxhve/UJASrz0B5ihlP4Q==", + "license": "MIT" + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -4955,7 +5324,6 @@ "version": "20.19.35", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", "integrity": "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -4988,6 +5356,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -5713,6 +6088,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -6560,6 +6948,17 @@ "node": ">=6.6.0" } }, + "node_modules/core-js": { + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz", + "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -6609,7 +7008,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -6848,6 +7246,13 @@ "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==", "license": "MIT" }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -7032,6 +7437,15 @@ "node": ">=0.10.0" } }, + "node_modules/dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dotenv": { "version": "17.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", @@ -7060,6 +7474,13 @@ "node": ">= 0.4" } }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true, + "license": "MIT" + }, "node_modules/eciesjs": { "version": "0.4.17", "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.17.tgz", @@ -8104,6 +8525,12 @@ "node": "^12.20 || >= 14.13" } }, + "node_modules/fflate": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", + "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==", + "license": "MIT" + }, "node_modules/figures": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", @@ -8564,6 +8991,22 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -8692,6 +9135,13 @@ "node": ">=16.9.0" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -9233,6 +9683,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -9451,7 +9911,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/iterator.prototype": { @@ -9990,6 +10449,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -10169,6 +10634,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -10677,6 +11152,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -10872,7 +11357,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -11013,6 +11497,27 @@ "node": ">=4" } }, + "node_modules/posthog-js": { + "version": "1.357.2", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.357.2.tgz", + "integrity": "sha512-f/7z56Xd7BC1TtWCzVVjU7m6NaiXiIK0Gc9shlwhi7weWt+OxJe59gNB6/etDoJHI9/Il8cjeXFZjAl+CA6ybQ==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/api-logs": "^0.208.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.208.0", + "@opentelemetry/resources": "^2.2.0", + "@opentelemetry/sdk-logs": "^0.208.0", + "@posthog/core": "1.23.2", + "@posthog/types": "1.357.2", + "core-js": "^3.38.1", + "dompurify": "^3.3.1", + "fflate": "^0.4.8", + "preact": "^10.28.2", + "query-selector-shadow-dom": "^1.0.1", + "web-vitals": "^5.1.0" + } + }, "node_modules/powershell-utils": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", @@ -11026,6 +11531,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/preact": { + "version": "10.28.4", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.4.tgz", + "integrity": "sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -11088,6 +11603,30 @@ "react-is": "^16.13.1" } }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -11128,6 +11667,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/query-selector-shadow-dom": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz", + "integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==", + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -12072,7 +12617,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -12085,7 +12629,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -12187,6 +12730,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -12194,6 +12752,16 @@ "dev": true, "license": "MIT" }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -12703,6 +13271,16 @@ "node": ">=0.6" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/tough-cookie": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", @@ -12965,7 +13543,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unicorn-magic": { @@ -13410,11 +13987,54 @@ "node": ">= 8" } }, + "node_modules/web-vitals": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.1.0.tgz", + "integrity": "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==", + "license": "Apache-2.0" + }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz", + "integrity": "sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "is-plain-object": "^5.0.0", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -13609,6 +14229,28 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/wsl-utils": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", diff --git a/package.json b/package.json index a039c76..539e679 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,10 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "analyze": "ANALYZE=true next build", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui" }, "dependencies": { "@dnd-kit/core": "^6.3.1", @@ -20,17 +23,21 @@ "date-fns": "^4.1.0", "lucide-react": "^0.576.0", "next": "16.1.6", + "posthog-js": "^1.357.2", "radix-ui": "^1.4.3", "react": "19.2.3", "react-day-picker": "^9.14.0", "react-dom": "19.2.3", "react-hook-form": "^7.71.2", "recharts": "^3.7.0", + "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "zod": "^4.3.6", "zustand": "^5.0.11" }, "devDependencies": { + "@axe-core/playwright": "^4.11.1", + "@next/bundle-analyzer": "^16.1.6", "@playwright/test": "^1.58.2", "@tailwindcss/postcss": "^4", "@types/node": "^20", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..86e994c --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./tests", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: "html", + use: { + baseURL: "http://localhost:3000", + screenshot: "only-on-failure", + trace: "on-first-retry", + }, + projects: [ + { + name: "Desktop Chrome", + use: { ...devices["Desktop Chrome"] }, + }, + { + name: "Mobile Safari", + use: { ...devices["iPhone 14"] }, + }, + { + name: "Tablet", + use: { viewport: { width: 768, height: 1024 } }, + }, + ], + webServer: { + command: "npm run dev", + url: "http://localhost:3000", + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/src/app/(auth)/forgot-password/page.tsx b/src/app/(auth)/forgot-password/page.tsx index c5deba7..8b289e2 100644 --- a/src/app/(auth)/forgot-password/page.tsx +++ b/src/app/(auth)/forgot-password/page.tsx @@ -64,7 +64,7 @@ export default function ForgotPasswordPage() { >
{error && ( -
+
{error}
)} @@ -77,10 +77,11 @@ export default function ForgotPasswordPage() { placeholder="you@example.com" autoComplete="email" aria-invalid={!!errors.email} + aria-describedby={errors.email ? "email-error" : undefined} {...register("email")} /> {errors.email && ( -

{errors.email.message}

+ )}
diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index 0f6c673..88b1595 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -44,7 +44,7 @@ export default function LoginPage() { > {error && ( -
+
{error}
)} @@ -56,10 +56,11 @@ export default function LoginPage() { placeholder="you@example.com" autoComplete="username" aria-invalid={!!errors.username} + aria-describedby={errors.username ? "username-error" : undefined} {...register("username")} /> {errors.username && ( -

+

)} @@ -79,10 +80,11 @@ export default function LoginPage() { id="password" autoComplete="current-password" aria-invalid={!!errors.password} + aria-describedby={errors.password ? "password-error" : undefined} {...register("password")} /> {errors.password && ( -

+

)} diff --git a/src/app/(auth)/register/page.tsx b/src/app/(auth)/register/page.tsx index a443c03..6367bfd 100644 --- a/src/app/(auth)/register/page.tsx +++ b/src/app/(auth)/register/page.tsx @@ -59,7 +59,7 @@ export default function RegisterPage() { > {error && ( -
+
{error}
)} @@ -71,10 +71,11 @@ export default function RegisterPage() { id="first_name" autoComplete="given-name" aria-invalid={!!errors.first_name} + aria-describedby={errors.first_name ? "first-name-error" : undefined} {...register("first_name")} /> {errors.first_name && ( -

+

)} @@ -86,10 +87,11 @@ export default function RegisterPage() { id="last_name" autoComplete="family-name" aria-invalid={!!errors.last_name} + aria-describedby={errors.last_name ? "last-name-error" : undefined} {...register("last_name")} /> {errors.last_name && ( -

+

)} @@ -102,10 +104,11 @@ export default function RegisterPage() { id="username" autoComplete="username" aria-invalid={!!errors.username} + aria-describedby={errors.username ? "username-error" : undefined} {...register("username")} /> {errors.username && ( -

+

)} @@ -119,10 +122,11 @@ export default function RegisterPage() { placeholder="you@example.com" autoComplete="email" aria-invalid={!!errors.email} + aria-describedby={errors.email ? "email-error" : undefined} {...register("email")} /> {errors.email && ( -

{errors.email.message}

+ )}
@@ -132,10 +136,11 @@ export default function RegisterPage() { id="password" autoComplete="new-password" aria-invalid={!!errors.password} + aria-describedby={errors.password ? "password-error" : undefined} {...register("password")} /> {errors.password && ( -

+

)} @@ -147,10 +152,11 @@ export default function RegisterPage() { id="confirm_password" autoComplete="new-password" aria-invalid={!!errors.confirm_password} + aria-describedby={errors.confirm_password ? "confirm-password-error" : undefined} {...register("confirm_password")} /> {errors.confirm_password && ( -

+

)} diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index 3271b67..a60330c 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -11,7 +11,7 @@ import { NextRequest, NextResponse } from 'next/server'; const API_BASE_URL = process.env.API_URL || process.env.NEXT_PUBLIC_API_URL || - 'https://mycrib.treytartt.com/api'; + 'https://casera.treytartt.com/api'; const COOKIE_NAME = 'casera-token'; const COOKIE_MAX_AGE = 60 * 60 * 24 * 30; // 30 days diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts index 363b32f..27b8352 100644 --- a/src/app/api/auth/logout/route.ts +++ b/src/app/api/auth/logout/route.ts @@ -11,7 +11,7 @@ import { NextRequest, NextResponse } from 'next/server'; const API_BASE_URL = process.env.API_URL || process.env.NEXT_PUBLIC_API_URL || - 'https://mycrib.treytartt.com/api'; + 'https://casera.treytartt.com/api'; const COOKIE_NAME = 'casera-token'; diff --git a/src/app/api/auth/me/route.ts b/src/app/api/auth/me/route.ts index ca44640..79a076c 100644 --- a/src/app/api/auth/me/route.ts +++ b/src/app/api/auth/me/route.ts @@ -11,7 +11,7 @@ import { NextRequest, NextResponse } from 'next/server'; const API_BASE_URL = process.env.API_URL || process.env.NEXT_PUBLIC_API_URL || - 'https://mycrib.treytartt.com/api'; + 'https://casera.treytartt.com/api'; const COOKIE_NAME = 'casera-token'; diff --git a/src/app/api/proxy/[...path]/route.ts b/src/app/api/proxy/[...path]/route.ts index 07f2753..c9c6bec 100644 --- a/src/app/api/proxy/[...path]/route.ts +++ b/src/app/api/proxy/[...path]/route.ts @@ -12,11 +12,11 @@ import { NextRequest, NextResponse } from 'next/server'; const API_BASE_URL = process.env.API_URL || process.env.NEXT_PUBLIC_API_URL || - 'https://mycrib.treytartt.com/api'; + 'https://casera.treytartt.com/api'; /** * Build the target URL from the catch-all path segments. - * e.g. /api/proxy/tasks/123/ -> https://mycrib.treytartt.com/api/tasks/123/ + * e.g. /api/proxy/tasks/123/ -> https://casera.treytartt.com/api/tasks/123/ */ function buildTargetUrl(request: NextRequest, pathSegments: string[]): string { const path = `/${pathSegments.join('/')}`; diff --git a/src/app/app/contractors/[id]/edit/page.tsx b/src/app/app/contractors/[id]/edit/page.tsx index 0f6e13d..c9b3984 100644 --- a/src/app/app/contractors/[id]/edit/page.tsx +++ b/src/app/app/contractors/[id]/edit/page.tsx @@ -2,11 +2,13 @@ import { use } from "react"; import { useRouter } from "next/navigation"; +import { toast } from "sonner"; import { PageHeader } from "@/components/shared/page-header"; import { LoadingSkeleton } from "@/components/shared/loading-skeleton"; import { ErrorBanner } from "@/components/shared/error-banner"; import { ContractorForm } from "@/components/contractors/contractor-form"; import { useContractor, useUpdateContractor } from "@/lib/hooks/use-contractors"; +import { useDataProvider } from "@/lib/demo/data-provider-context"; import type { ContractorFormValues } from "@/components/contractors/contractor-form"; export default function EditContractorPage({ @@ -17,6 +19,7 @@ export default function EditContractorPage({ const { id: idParam } = use(params); const id = Number(idParam); const router = useRouter(); + const { basePath } = useDataProvider(); const { data: contractor, isLoading, isError, error, refetch } = useContractor(id); const updateContractor = useUpdateContractor(id); @@ -24,7 +27,11 @@ export default function EditContractorPage({ function handleSubmit(data: ContractorFormValues) { updateContractor.mutate(data, { onSuccess: () => { - router.push(`/app/contractors/${id}`); + toast.success("Contractor updated"); + router.push(`${basePath}/contractors/${id}`); + }, + onError: () => { + toast.error("Failed to update contractor"); }, }); } @@ -45,7 +52,7 @@ export default function EditContractorPage({ if (!contractor) return null; return ( -
+
{ - router.push("/app/contractors"); + toast.success("Contractor deleted"); + router.push(`${basePath}/contractors`); + }, + onError: () => { + toast.error("Failed to delete contractor"); }, }); } diff --git a/src/app/app/contractors/new/page.tsx b/src/app/app/contractors/new/page.tsx index 4d4d3e3..4efce98 100644 --- a/src/app/app/contractors/new/page.tsx +++ b/src/app/app/contractors/new/page.tsx @@ -1,25 +1,32 @@ "use client"; import { useRouter } from "next/navigation"; +import { toast } from "sonner"; import { PageHeader } from "@/components/shared/page-header"; import { ContractorForm } from "@/components/contractors/contractor-form"; import { useCreateContractor } from "@/lib/hooks/use-contractors"; +import { useDataProvider } from "@/lib/demo/data-provider-context"; import type { ContractorFormValues } from "@/components/contractors/contractor-form"; export default function NewContractorPage() { const router = useRouter(); + const { basePath } = useDataProvider(); const createContractor = useCreateContractor(); function handleSubmit(data: ContractorFormValues) { createContractor.mutate(data, { onSuccess: (res) => { - router.push(`/app/contractors/${res.id}`); + toast.success("Contractor created"); + router.push(`${basePath}/contractors/${res.id}`); + }, + onError: () => { + toast.error("Failed to create contractor"); }, }); } return ( -
+
diff --git a/src/app/app/contractors/page.tsx b/src/app/app/contractors/page.tsx index 0fcbcf2..3d7eaf9 100644 --- a/src/app/app/contractors/page.tsx +++ b/src/app/app/contractors/page.tsx @@ -15,9 +15,11 @@ import { CaseraFileImport } from "@/components/sharing/casera-file-handler"; import { ContractorCard } from "@/components/contractors/contractor-card"; import { ContractorFilters } from "@/components/contractors/contractor-filters"; import { useContractors, useToggleFavorite, useCreateContractor } from "@/lib/hooks/use-contractors"; +import { useDataProvider } from "@/lib/demo/data-provider-context"; export default function ContractorsPage() { const router = useRouter(); + const { basePath } = useDataProvider(); const { data: contractors, isLoading, isError, error, refetch } = useContractors(); const toggleFavorite = useToggleFavorite(); const createContractor = useCreateContractor(); @@ -29,7 +31,7 @@ export default function ContractorsPage() { const [importError, setImportError] = useState(null); const filtered = useMemo(() => { - if (!contractors) return []; + if (!Array.isArray(contractors)) return []; let list = contractors; // Search filter (name or company) @@ -105,7 +107,7 @@ export default function ContractorsPage() { title="Contractors" description="Manage your trusted contractors and service providers" actionLabel="Add Contractor" - onAction={() => router.push("/app/contractors/new")} + onAction={() => router.push(`${basePath}/contractors/new`)} > +
+ + {/* Login link */} +

+ Already have an account?{" "} + + Log In + +

+
+
+ ); +} diff --git a/src/app/error.tsx b/src/app/error.tsx new file mode 100644 index 0000000..b73716f --- /dev/null +++ b/src/app/error.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { AlertTriangle } from "lucide-react"; + +export default function Error({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + return ( +
+
+ +
+

Something went wrong

+

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

+ +
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b8da2af..e6b53a2 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,7 +1,10 @@ import type { Metadata } from "next"; +import { Suspense } from "react"; import { Geist, Geist_Mono } from "next/font/google"; import { ThemeProvider } from "@/lib/themes/theme-provider"; import { QueryProvider } from "@/lib/query/query-provider"; +import { PostHogProvider } from "@/lib/analytics/posthog-provider"; +import { Toaster } from "@/components/ui/sonner"; import "./globals.css"; const geistSans = Geist({ @@ -15,8 +18,24 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Casera", - description: "Property management platform", + title: { + default: "Casera โ€” Home Maintenance Made Simple", + template: "%s | Casera", + }, + description: + "Track tasks, organize contractors, store documents. Manage your home maintenance in one place.", + openGraph: { + title: "Casera โ€” Home Maintenance Made Simple", + description: + "Track tasks, organize contractors, store documents. Manage your home maintenance in one place.", + type: "website", + siteName: "Casera", + }, + twitter: { + card: "summary_large_image", + title: "Casera โ€” Home Maintenance Made Simple", + description: "Home Maintenance Made Simple", + }, }; export default function RootLayout({ @@ -29,9 +48,16 @@ export default function RootLayout({ - - {children} - + + + + + {children} + + + + + ); diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 0000000..caa1dd7 --- /dev/null +++ b/src/app/not-found.tsx @@ -0,0 +1,20 @@ +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { FileQuestion } from "lucide-react"; + +export default function NotFound() { + return ( +
+
+ +
+

Page not found

+

+ The page you're looking for doesn't exist or has been moved. +

+ +
+ ); +} diff --git a/src/components/contractors/contractor-card.tsx b/src/components/contractors/contractor-card.tsx index 92b9b62..5726f2e 100644 --- a/src/components/contractors/contractor-card.tsx +++ b/src/components/contractors/contractor-card.tsx @@ -5,6 +5,7 @@ import { Phone, Mail, Star } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardAction } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { useDataProvider } from "@/lib/demo/data-provider-context"; import type { ContractorResponse } from "@/lib/api/contractors"; interface ContractorCardProps { @@ -13,10 +14,11 @@ interface ContractorCardProps { } export function ContractorCard({ contractor, onToggleFavorite }: ContractorCardProps) { + const { basePath } = useDataProvider(); return ( - + {contractor.name} {contractor.company && ( @@ -27,12 +29,14 @@ export function ContractorCard({ contractor, onToggleFavorite }: ContractorCardP variant="ghost" size="icon" className="size-8" + aria-label={contractor.is_favorite ? "Remove from favorites" : "Add to favorites"} onClick={(e) => { e.preventDefault(); onToggleFavorite(contractor.id); }} >