diff --git a/package.json b/package.json index 0bab49b..f0b4943 100644 --- a/package.json +++ b/package.json @@ -31,9 +31,11 @@ "analyze": "ANALYZE=true next build --webpack" }, "dependencies": { + "@anthropic-ai/sdk": "^0.68.0", "@auth/prisma-adapter": "^2.11.2", "@formatjs/intl-localematcher": "^0.8.3", "@hookform/resolvers": "^5.2.2", + "@marsidev/react-turnstile": "^1.4.0", "@neondatabase/serverless": "^1.1.0", "@prisma/adapter-neon": "^7.8.0", "@prisma/adapter-pg": "^7.8.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9fe6d2..d434a6d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@anthropic-ai/sdk': + specifier: ^0.68.0 + version: 0.68.0(zod@4.3.6) '@auth/prisma-adapter': specifier: ^2.11.2 version: 2.11.2(@prisma/client@7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(magicast@0.5.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3))(typescript@6.0.3)) @@ -17,6 +20,9 @@ importers: '@hookform/resolvers': specifier: ^5.2.2 version: 5.2.2(react-hook-form@7.73.1(react@19.2.5)) + '@marsidev/react-turnstile': + specifier: ^1.4.0 + version: 1.5.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@neondatabase/serverless': specifier: ^1.1.0 version: 1.1.0 @@ -268,6 +274,15 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@anthropic-ai/sdk@0.68.0': + resolution: {integrity: sha512-SMYAmbbiprG8k1EjEPMTwaTqssDT7Ae+jxcR5kWXiqTlbwMR2AthXtscEVWOHkRfyAV5+y3PFYTJRNa3OJWIEw==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + '@asamuzakjp/css-color@5.1.11': resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -972,6 +987,12 @@ packages: '@mapbox/vector-tile@2.0.4': resolution: {integrity: sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==} + '@marsidev/react-turnstile@1.5.2': + resolution: {integrity: sha512-+3aBPxp86JzSC0ZmgyonoGoUEENcUkH3LGahXSpkV87ArvD2DzRCmPgh0FyQk6PQRmJwQJDAfwNavFsxUxMQWA==} + peerDependencies: + react: ^17.0.2 || ^18.0.0 || ^19.0 + react-dom: ^17.0.2 || ^18.0.0 || ^19.0 + '@napi-rs/wasm-runtime@0.2.10': resolution: {integrity: sha512-bCsCyeZEwVErsGmyPNSzwfwFn4OdxBj0mmv6hOFucB/k81Ojdu68RbZdxYsRQUPc9l6SU5F/cG+bXgWs3oUgsQ==} @@ -3850,6 +3871,10 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -5011,6 +5036,9 @@ packages: resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} engines: {node: '>=20'} + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -5393,6 +5421,12 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@anthropic-ai/sdk@0.68.0(zod@4.3.6)': + dependencies: + json-schema-to-ts: 3.1.1 + optionalDependencies: + zod: 4.3.6 + '@asamuzakjp/css-color@5.1.11': dependencies: '@asamuzakjp/generational-cache': 1.0.1 @@ -6045,6 +6079,11 @@ snapshots: '@types/geojson': 7946.0.16 pbf: 4.0.1 + '@marsidev/react-turnstile@1.5.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + '@napi-rs/wasm-runtime@0.2.10': dependencies: '@emnapi/core': 1.4.3 @@ -9198,6 +9237,11 @@ snapshots: json-parse-even-better-errors@2.3.1: {} + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.29.2 + ts-algebra: 2.0.0 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -10489,6 +10533,8 @@ snapshots: dependencies: punycode: 2.3.1 + ts-algebra@2.0.0: {} + ts-api-utils@2.5.0(typescript@6.0.3): dependencies: typescript: 6.0.3 diff --git a/src/app/[lang]/page.tsx b/src/app/[lang]/page.tsx index f51edaf..16cca7e 100644 --- a/src/app/[lang]/page.tsx +++ b/src/app/[lang]/page.tsx @@ -1,4 +1,3 @@ -import { Suspense } from "react"; import { Metadata } from "next"; import { getListings } from "@/components/host/actions"; import { createMetadata } from "@/lib/metadata"; @@ -29,6 +28,9 @@ async function getPublishedListings(): Promise { } } +// Note: no here. The colocated `loading.tsx` already provides the +// streaming fallback for this segment; adding a redundant Suspense boundary +// triggers Next 16 Turbopack's "$RS parentNode null" hydration race. export default async function HomePage({ params, }: { @@ -37,15 +39,5 @@ export default async function HomePage({ const { lang } = await params; const listings = await getPublishedListings(); - return ( - - - - } - > - - - ); + return ; } diff --git a/src/app/error.tsx b/src/app/error.tsx index 39e69bb..5d67890 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -1,8 +1,30 @@ 'use client'; import { useEffect } from 'react'; -import { Button } from '@/components/ui/button'; -import { AlertTriangle } from 'lucide-react'; + +// Inline SVG + native buttons instead of `lucide-react` / `@/components/ui/button`: +// Turbopack's error.tsx boundary fires before lazy chunks finish loading, so any +// external component import fails with "module factory is not available". Keep +// this file dependency-light — only `react` is safe to import here. +function AlertTriangle({ className }: { className?: string }) { + return ( + + ); +} export default function Error({ error, @@ -60,19 +82,18 @@ export default function Error({
- - + +

diff --git a/src/components/report-issue.tsx b/src/components/report-issue.tsx deleted file mode 100644 index ab08cf5..0000000 --- a/src/components/report-issue.tsx +++ /dev/null @@ -1,138 +0,0 @@ -"use client" - -import { useState } from "react" -import { usePathname } from "next/navigation" -import { Bug } from "lucide-react" - -import { reportIssue } from "@/lib/actions/report-issue" -import { Button } from "@/components/ui/button" -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog" - -const translations = { - en: { - link: "Report an issue", - title: "Report an issue", - placeholder: "Describe the issue...", - submit: "Submit", - submitting: "Submitting...", - success: "Submitted. Thank you!", - error: "Something went wrong. Try again.", - }, - ar: { - link: "\u0627\u0644\u0625\u0628\u0644\u0627\u063a \u0639\u0646 \u0645\u0634\u0643\u0644\u0629", - title: "\u0627\u0644\u0625\u0628\u0644\u0627\u063a \u0639\u0646 \u0645\u0634\u0643\u0644\u0629", - placeholder: "\u0635\u0641 \u0627\u0644\u0645\u0634\u0643\u0644\u0629...", - submit: "\u0625\u0631\u0633\u0627\u0644", - submitting: "\u062c\u0627\u0631\u064a \u0627\u0644\u0625\u0631\u0633\u0627\u0644...", - success: "\u062a\u0645 \u0627\u0644\u0625\u0631\u0633\u0627\u0644. \u0634\u0643\u0631\u0627\u064b \u0644\u0643!", - error: "\u062d\u062f\u062b \u062e\u0637\u0623. \u062d\u0627\u0648\u0644 \u0645\u0631\u0629 \u0623\u062e\u0631\u0649.", - }, -} as const - -interface ReportIssueProps { - variant?: "text" | "icon" -} - -function parseBrowser(ua: string): string { - if (ua.includes("Firefox/")) return `Firefox / ${getOS(ua)}` - if (ua.includes("Edg/")) return `Edge / ${getOS(ua)}` - if (ua.includes("Chrome/")) return `Chrome / ${getOS(ua)}` - if (ua.includes("Safari/")) return `Safari / ${getOS(ua)}` - return ua.slice(0, 50) -} - -function getOS(ua: string): string { - if (ua.includes("Mac OS")) return "macOS" - if (ua.includes("Windows")) return "Windows" - if (ua.includes("Android")) return "Android" - if (ua.includes("iPhone") || ua.includes("iPad")) return "iOS" - if (ua.includes("Linux")) return "Linux" - return "Unknown" -} - -export function ReportIssue({ variant = "text" }: ReportIssueProps) { - const [open, setOpen] = useState(false) - const [description, setDescription] = useState("") - const [status, setStatus] = useState< - "idle" | "loading" | "success" | "error" - >("idle") - const pathname = usePathname() - const t = translations[pathname?.startsWith("/ar") ? "ar" : "en"] - - async function handleSubmit() { - if (!description.trim()) return - setStatus("loading") - try { - await reportIssue({ - description, - pageUrl: window.location.href, - viewport: `${window.innerWidth}x${window.innerHeight}`, - direction: document.documentElement.dir || "ltr", - browser: parseBrowser(navigator.userAgent), - }) - setStatus("success") - setDescription("") - setTimeout(() => { - setOpen(false) - setStatus("idle") - }, 1500) - } catch { - setStatus("error") - } - } - - return ( -

{ - setOpen(v) - if (!v) setStatus("idle") - }} - > - - {variant === "icon" ? ( - - ) : ( - - )} - - - - {t.title} - -