diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx index 5856fc2..a8984af 100644 --- a/app/(auth)/layout.tsx +++ b/app/(auth)/layout.tsx @@ -1,7 +1,6 @@ import { MarketingInfo } from "@/components/marketing-info"; -import { auth } from "@/lib/auth"; import { redirect } from 'next/navigation' -import { headers } from "next/headers"; +import { getServerSession } from "@/lib/server/session"; @@ -10,9 +9,7 @@ export default async function AuthLayout({ }: Readonly<{ children: React.ReactNode; }>) { - const session = await auth.api.getSession({ - headers: await headers(), - }); + const session = await getServerSession(); if (session) redirect("/"); diff --git a/app/(dashboard)/invite/accept/page.tsx b/app/(dashboard)/invite/accept/page.tsx index 6ed1811..540567e 100644 --- a/app/(dashboard)/invite/accept/page.tsx +++ b/app/(dashboard)/invite/accept/page.tsx @@ -1,6 +1,4 @@ import { eq, and } from "drizzle-orm"; -import { auth } from "@/lib/auth"; -import { headers } from "next/headers"; import { redirect } from "next/navigation"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; @@ -8,6 +6,7 @@ import Link from "next/link"; import { databaseDrizzle } from "@/db"; import { invitation, usersProjects } from "@/db/schema"; import { LogoutBtn } from "@/components/LogoutBtn/LogoutBtn"; +import { getServerSession } from "@/lib/server/session"; interface PageProps { searchParams: Promise<{ token?: string }>; @@ -19,7 +18,7 @@ export default async function AcceptInvitePage({ searchParams }: PageProps) { return ; } - const session = await auth.api.getSession({ headers: await headers() }); + const session = await getServerSession(); if (!session?.user.id) { return ( diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx index f483e1e..687291c 100644 --- a/app/(dashboard)/layout.tsx +++ b/app/(dashboard)/layout.tsx @@ -1,8 +1,6 @@ -import { auth } from "@/lib/auth"; import { redirect } from 'next/navigation' -import { headers } from "next/headers"; import { TooltipProvider } from "@/components/ui/tooltip"; -import { ThemeProvider } from "@/components/theme-provider"; +import { getServerSession } from "@/lib/server/session"; export default async function RootLayout({ @@ -10,23 +8,14 @@ export default async function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - const session = await auth.api.getSession({ - headers: await headers(), - }); + const session = await getServerSession(); if (!session) redirect("/signin"); return ( - - - {children} - - + + {children} + ); } diff --git a/app/(dashboard)/projects/[id]/changelog/page.tsx b/app/(dashboard)/projects/[id]/changelog/page.tsx index 023df97..6de25ec 100644 --- a/app/(dashboard)/projects/[id]/changelog/page.tsx +++ b/app/(dashboard)/projects/[id]/changelog/page.tsx @@ -1,44 +1,41 @@ import { ChangelogList } from "@/components/ChangelogList/ChangelogList" import { UpsertChangelog } from "@/components/UpsertChangelog/UpsertChangelog" import { databaseDrizzle } from "@/db" -import { auth } from "@/lib/auth" import { permission } from "@/lib/utils" -import { headers } from "next/headers"; import { redirect } from 'next/navigation' +import { getServerSession } from "@/lib/server/session" export default async function page({ params }: { params: Promise<{ id: string }> }) { - const { id } = await params - const session = await auth.api.getSession({ headers: await headers() }); + const [{ id }, session] = await Promise.all([params, getServerSession()]) if (!session?.user.id) return redirect("/signin"); - const logs = await databaseDrizzle.query.changelogs.findMany({ - where: (c, ops) => ops.eq(c.projectId, id), - with: { - user: { - columns: { - id: true, - name: true, - image: true, - }, - with: { - usersProjects: { - where: (p, ops) => ops.eq(p.projectId, id), - columns: { - role: true, + const [logs, memberships] = await Promise.all([ + databaseDrizzle.query.changelogs.findMany({ + where: (c, ops) => ops.eq(c.projectId, id), + with: { + user: { + columns: { + id: true, + name: true, + image: true, + }, + with: { + usersProjects: { + where: (p, ops) => ops.eq(p.projectId, id), + columns: { + role: true, + } } } } } - } - }) - - - // Get memberships for role checking - const memberships = await databaseDrizzle.query.usersProjects.findMany({ - where: (c, ops) => ops.eq(c.projectId, id), - columns: { userId: true, role: true }, - }); + }), + databaseDrizzle.query.usersProjects.findMany({ + where: (c, ops) => ops.eq(c.projectId, id), + columns: { userId: true, role: true }, + }) + ]) const permit = permission(memberships, session.user.id) diff --git a/app/(dashboard)/projects/[id]/feature-requests/page.tsx b/app/(dashboard)/projects/[id]/feature-requests/page.tsx index 9f5ad11..5260396 100644 --- a/app/(dashboard)/projects/[id]/feature-requests/page.tsx +++ b/app/(dashboard)/projects/[id]/feature-requests/page.tsx @@ -1,7 +1,5 @@ import { notFound } from "next/navigation"; import { databaseDrizzle } from "@/db"; -import { auth } from "@/lib/auth"; -import { headers } from "next/headers"; import { redirect } from 'next/navigation' import { UpsertFeature } from "@/components/UpsertFeature/UpsertFeature"; import { UpvoteProvider } from "@/contexts/UpvoteProvider"; @@ -11,6 +9,7 @@ import { feature, featureTags, project, usersProjects } from "@/db/schema"; import { FeatureFilters } from "@/components/FeatureFilters/FeatureFilters"; import { FeatureList } from "@/components/FeatureLists/FeatureList"; import { FeatureDetail } from "@/components/FeatureDetail/FeatureDetail"; +import { getServerSession } from "@/lib/server/session"; interface PageProps { @@ -57,37 +56,29 @@ export interface QFeature { }[]; } - +type FeatureStatus = QFeature["status"] export default async function ProjectFeedbackPage({ params, searchParams }: PageProps) { - const { id } = await params; - const { + const [{ id }, { status = "all", sort = "most-votes", q = "", tags = "", page = "1", featureId, - } = await searchParams; - const session = await auth.api.getSession({ headers: await headers() }); + }] = await Promise.all([params, searchParams]); + const session = await getServerSession(); if (!session?.user.id) return redirect("/signin"); const currentPage = parseInt(page) || 1; const pageSize = 10; const tagIds = tags ? tags.split(",").filter(Boolean) : []; - - // Fetch project and its tags - const projectData = await databaseDrizzle.query.project.findFirst({ - where: eq(project.id, id), - columns: { name: true }, - with: { tags: true }, - }); - if (!projectData) return notFound(); + const featureStatus = status === "all" ? undefined : (status as FeatureStatus) // Build where clause for features const featureWhere = and( eq(feature.projectId, id), - status !== "all" ? eq(feature.status, status as any) : undefined, + featureStatus ? eq(feature.status, featureStatus) : undefined, q ? or( ilike(feature.title, `%${q}%`), @@ -105,46 +96,51 @@ export default async function ProjectFeedbackPage({ params, searchParams }: Page : undefined ); - // Fetch features with pagination - const features = await databaseDrizzle.query.feature.findMany({ - where: featureWhere, - orderBy: (f, { desc, asc }) => { - if (sort === "most-votes") return desc(f.upvotesCount); - if (sort === "least-votes") return asc(f.upvotesCount); - if (sort === "oldest") return asc(f.createdAt); - return desc(f.createdAt); // newest default - }, - limit: pageSize, - offset: (currentPage - 1) * pageSize, - with: { - comments: { columns: { id: true } }, // only count for list - upvotes: { - where: (u, ops) => - ops.or( - ops.eq(u.voterToken, session.user.id), - ops.eq(u.voterEmail, session.user.email) - ), - columns: { id: true }, + const [projectData, features, totalCountResult, memberships] = await Promise.all([ + databaseDrizzle.query.project.findFirst({ + where: eq(project.id, id), + columns: { name: true }, + with: { tags: true }, + }), + databaseDrizzle.query.feature.findMany({ + where: featureWhere, + orderBy: (f, { desc, asc }) => { + if (sort === "most-votes") return desc(f.upvotesCount); + if (sort === "least-votes") return asc(f.upvotesCount); + if (sort === "oldest") return asc(f.createdAt); + return desc(f.createdAt); + }, + limit: pageSize, + offset: (currentPage - 1) * pageSize, + with: { + comments: { columns: { id: true } }, + upvotes: { + where: (u, ops) => + ops.or( + ops.eq(u.voterToken, session.user.id), + ops.eq(u.voterEmail, session.user.email) + ), + columns: { id: true }, + }, + tags: { with: { tag: true } }, }, - tags: { with: { tag: true } }, - }, - }); + }), + databaseDrizzle + .select({ count: sql`count(*)` }) + .from(feature) + .where(featureWhere) + .limit(1), + databaseDrizzle.query.usersProjects.findMany({ + where: eq(usersProjects.projectId, id), + columns: { userId: true, role: true }, + }) + ]); + + if (!projectData) return notFound(); - // Count total for pagination - const totalCountResult = await databaseDrizzle - .select({ count: sql`count(*)` }) - .from(feature) - .where(featureWhere) - .limit(1); const totalCount = totalCountResult[0]?.count ?? 0; const totalPages = Math.ceil(totalCount / pageSize); - // Get memberships for role checking - const memberships = await databaseDrizzle.query.usersProjects.findMany({ - where: eq(usersProjects.projectId, id), - columns: { userId: true, role: true }, - }); - const permit = permission(memberships, session.user.id) return ( diff --git a/app/(dashboard)/projects/[id]/layout.tsx b/app/(dashboard)/projects/[id]/layout.tsx index 874713d..9c86240 100644 --- a/app/(dashboard)/projects/[id]/layout.tsx +++ b/app/(dashboard)/projects/[id]/layout.tsx @@ -6,11 +6,10 @@ import { SidebarProvider, SidebarTrigger, } from "@/components/ui/sidebar" -import { auth } from "@/lib/auth"; import { redirect } from 'next/navigation' -import { headers } from "next/headers"; import { databaseDrizzle } from "@/db"; import { UserProject } from "@/type"; +import { getServerSession } from "@/lib/server/session"; export default async function ProjectLayout({ children, @@ -19,11 +18,7 @@ export default async function ProjectLayout({ children: React.ReactNode; params: Promise<{ id: string }> }>) { - const {id} = await params - - const session = await auth.api.getSession({ - headers: await headers(), - }); + const [{ id }, session] = await Promise.all([params, getServerSession()]) if (!session?.user.id) return redirect("/signin") diff --git a/app/(dashboard)/projects/[id]/loading.tsx b/app/(dashboard)/projects/[id]/loading.tsx new file mode 100644 index 0000000..093a03d --- /dev/null +++ b/app/(dashboard)/projects/[id]/loading.tsx @@ -0,0 +1,15 @@ +import { Skeleton } from "@/components/ui/skeleton" + +export default function ProjectLoading() { + return ( +
+ +
+ + + +
+ +
+ ) +} diff --git a/app/(dashboard)/projects/[id]/page.tsx b/app/(dashboard)/projects/[id]/page.tsx index c03bfcd..06eac95 100644 --- a/app/(dashboard)/projects/[id]/page.tsx +++ b/app/(dashboard)/projects/[id]/page.tsx @@ -1,16 +1,19 @@ import { databaseDrizzle } from "@/db" -import { auth } from "@/lib/auth" -import { headers } from "next/headers" import { redirect } from "next/navigation" import { eq, desc } from 'drizzle-orm' import { notFound } from 'next/navigation' import { feature, project, widgetDailyStats } from "@/db/schema" import { TimeRange } from "@/type" import { getDateRange, getOverview } from "@/lib/analytics" -import { OverviewCards } from "@/components/analytics/OverviewCards" -import { TopFeaturesChart } from "@/components/analytics/TopFeaturesChart" -import { WidgetActivityChart } from "@/components/analytics/WidgetActivityChart" -import { ProductAdvisor } from "@/components/ProductAdvisor/ProductAdvisor" +import { getServerSession } from "@/lib/server/session" +import dynamic from "next/dynamic" +import { Suspense } from "react" +import { Skeleton } from "@/components/ui/skeleton" + +const OverviewCards = dynamic(() => import("@/components/analytics/OverviewCards").then((m) => m.OverviewCards)) +const TopFeaturesChart = dynamic(() => import("@/components/analytics/TopFeaturesChart").then((m) => m.TopFeaturesChart)) +const WidgetActivityChart = dynamic(() => import("@/components/analytics/WidgetActivityChart").then((m) => m.WidgetActivityChart)) +const ProductAdvisor = dynamic(() => import("@/components/ProductAdvisor/ProductAdvisor").then((m) => m.ProductAdvisor)) export default async function AnalyticsPage({ params, searchParams }: { @@ -21,58 +24,66 @@ export default async function AnalyticsPage({ params, searchParams }: { widgetRange?: TimeRange }> }) { - const { id } = await params - const { + const [{ id }, { overviewRange = 'month', topFeaturesRange = 'month', widgetRange = 'month' - } = await searchParams + }] = await Promise.all([params, searchParams]) - const session = await auth.api.getSession({ headers: await headers() }) + const session = await getServerSession() if (!session?.user.id) redirect('/signin') const topFeaturesStartEnd = getDateRange(topFeaturesRange) const widgetStartEnd = getDateRange(widgetRange) - const projectData = await databaseDrizzle.query.project.findFirst({ - where: eq(project.id, id), - columns: { id: true }, - with: { - features: { - where: (f, ops) => ops.and( - ops.gte(f.createdAt, topFeaturesStartEnd.startDate), - ops.lte(f.createdAt, topFeaturesStartEnd.endDate), - ops.gte(f.upvotesCount, 1) - ), - orderBy: desc(feature.upvotesCount), - limit: 10, - columns: { id: true, title: true, upvotesCount: true } - }, - widgetDailyStats: { - where: (w, ops) => ops.and( - ops.gte(w.date, widgetStartEnd.startDate), - ops.lte(w.date, widgetStartEnd.endDate) - ), - orderBy: desc(widgetDailyStats.date), - columns: { date: true, opens: true } + const [projectData, overviewData] = await Promise.all([ + databaseDrizzle.query.project.findFirst({ + where: eq(project.id, id), + columns: { id: true }, + with: { + features: { + where: (f, ops) => ops.and( + ops.gte(f.createdAt, topFeaturesStartEnd.startDate), + ops.lte(f.createdAt, topFeaturesStartEnd.endDate), + ops.gte(f.upvotesCount, 1) + ), + orderBy: desc(feature.upvotesCount), + limit: 10, + columns: { id: true, title: true, upvotesCount: true } + }, + widgetDailyStats: { + where: (w, ops) => ops.and( + ops.gte(w.date, widgetStartEnd.startDate), + ops.lte(w.date, widgetStartEnd.endDate) + ), + orderBy: desc(widgetDailyStats.date), + columns: { date: true, opens: true } + } } - } - }) + }), + getOverview(id, overviewRange) + ]) if (!projectData) notFound() - const overviewData = await getOverview(id, overviewRange) - return (

Analytics & Insights

- + }> + +

Product Advisor

- + }> + +
- - + }> + + + }> + +
) diff --git a/app/(dashboard)/projects/[id]/roadmap/page.tsx b/app/(dashboard)/projects/[id]/roadmap/page.tsx index ef27df4..466f352 100644 --- a/app/(dashboard)/projects/[id]/roadmap/page.tsx +++ b/app/(dashboard)/projects/[id]/roadmap/page.tsx @@ -1,11 +1,10 @@ import { and, eq, ilike, inArray, or } from "drizzle-orm"; -import { auth } from "@/lib/auth"; -import { headers } from "next/headers"; import { notFound, redirect } from "next/navigation"; import { databaseDrizzle } from "@/db"; import { feature, featureTags, project } from "@/db/schema"; import { RoadmapBoard } from "@/components/RoadmapBoard/RoadmapBoard"; import { FeatureFilters } from "@/components/FeatureFilters/FeatureFilters"; +import { getServerSession } from "@/lib/server/session"; interface PageProps { @@ -20,24 +19,25 @@ interface PageProps { }>; } +type FeatureStatus = "under_review" | "planned" | "in_progress" | "done" | "closed" + export default async function RoadmapPage({ params, searchParams }: PageProps) { - const { id } = await params; - const { + const [{ id }, { status = "all", sort = "most-votes", q = "", tags = "", - } = await searchParams; + }] = await Promise.all([params, searchParams]); const tagIds = tags ? tags.split(",").filter(Boolean) : []; + const featureStatus = status === "all" ? undefined : (status as FeatureStatus) - - const session = await auth.api.getSession({ headers: await headers() }); + const session = await getServerSession(); if (!session?.user.id) redirect("/signin"); // Build where clause for features const featureWhere = and( eq(feature.projectId, id), - status !== "all" ? eq(feature.status, status as any) : undefined, + featureStatus ? eq(feature.status, featureStatus) : undefined, q ? or( ilike(feature.title, `%${q}%`), diff --git a/app/(dashboard)/projects/[id]/team/page.tsx b/app/(dashboard)/projects/[id]/team/page.tsx index 4b9c5b7..6e9f052 100644 --- a/app/(dashboard)/projects/[id]/team/page.tsx +++ b/app/(dashboard)/projects/[id]/team/page.tsx @@ -1,19 +1,17 @@ import { eq } from "drizzle-orm"; -import { auth } from "@/lib/auth"; -import { headers } from "next/headers"; import { notFound, redirect } from "next/navigation"; import { databaseDrizzle } from "@/db"; import { usersProjects } from "@/db/schema"; import { TeamMembers } from "@/components/TeamMmbers/TeamMembers"; import { InviteForm } from "@/components/InviteForm/InviteForm"; +import { getServerSession } from "@/lib/server/session"; interface PageProps { params: Promise<{ id: string }>; } export default async function TeamPage({ params }: PageProps) { - const { id: projectId } = await params; - const session = await auth.api.getSession({ headers: await headers() }); + const [{ id: projectId }, session] = await Promise.all([params, getServerSession()]); if (!session?.user.id) redirect("/signin"); const members = await databaseDrizzle.query.usersProjects.findMany({ diff --git a/app/(dashboard)/projects/loading.tsx b/app/(dashboard)/projects/loading.tsx new file mode 100644 index 0000000..6441a5b --- /dev/null +++ b/app/(dashboard)/projects/loading.tsx @@ -0,0 +1,17 @@ +import { Skeleton } from "@/components/ui/skeleton" + +export default function ProjectsLoading() { + return ( +
+
+ + +
+
+ {Array.from({ length: 6 }).map((_, index) => ( + + ))} +
+
+ ) +} diff --git a/app/(dashboard)/projects/page.tsx b/app/(dashboard)/projects/page.tsx index d4e466b..dc785e9 100644 --- a/app/(dashboard)/projects/page.tsx +++ b/app/(dashboard)/projects/page.tsx @@ -1,10 +1,6 @@ -import { auth } from "@/lib/auth"; import { redirect } from 'next/navigation' -import { headers } from "next/headers"; import { databaseDrizzle } from "@/db"; import { Navbar } from "@/components/Navbar/Navbar"; -import * as motion from "motion/react-client" -import { Skeleton } from "@/components/ui/skeleton"; import { ArrowRight, FolderKanban } from "lucide-react"; import { Card, @@ -18,11 +14,10 @@ import { UpsertProject } from "@/components/UpsertProject/UpsertProject"; import { DeleteProject } from "@/components/DeleteProject/DeleteProject"; import { Badge } from "@/components/ui/badge" import Link from "next/link" +import { getServerSession } from "@/lib/server/session"; export default async function page() { - const session = await auth.api.getSession({ - headers: await headers(), - }); + const session = await getServerSession(); if (!session?.user.id) return redirect("/signin") const { name, email, image } = session.user @@ -38,51 +33,10 @@ export default async function page() { } }) - if (!userProjects) { - return ( - -
- - -
-
- {[1, 2, 3].map((i) => ( - - ))} -
-
- ); - } - - // Animation variants - const containerVariants = { - hidden: { opacity: 0 }, - visible: { - opacity: 1, - transition: { staggerChildren: 0.1, delayChildren: 0.2 }, - }, - }; - - const itemVariants = { - hidden: { y: 20, opacity: 0 }, - visible: { y: 0, opacity: 1 }, - }; - - return (
- +
{/* Header */}
@@ -97,8 +51,7 @@ export default async function page() { {/* Empty State */} {userProjects.length === 0 ? ( - @@ -110,15 +63,12 @@ export default async function page() { Create your first project to start collecting feedback from your users.

-
+
) : ( /* Project Grid */ - +
{userProjects.map(({ project, role }) => ( - +
@@ -158,12 +108,11 @@ export default async function page() { - - +
))} -
+
)} -
+
) } diff --git a/app/pub/[projectId]/features/page.tsx b/app/pub/[projectId]/features/page.tsx index 40a9cc3..3d6b97c 100644 --- a/app/pub/[projectId]/features/page.tsx +++ b/app/pub/[projectId]/features/page.tsx @@ -4,29 +4,45 @@ import { UpvoteProvider } from "@/contexts/UpvoteProvider"; import { databaseDrizzle } from "@/db"; import { cookies } from 'next/headers' import { notFound } from "next/navigation"; +import { eq, sql } from "drizzle-orm"; +import { feature } from "@/db/schema"; -export default async function page({ params, searchParams }: { params: Promise<{ projectId: string }>, searchParams: Promise<{ theme: string }> }) { +export default async function page({ params, searchParams }: { params: Promise<{ projectId: string }>, searchParams: Promise<{ theme?: string, page?: string }> }) { const { projectId } = await params - const { theme } = await searchParams + const { theme = "system", page = "1" } = await searchParams const cookieStore = await cookies() + const currentPage = Math.max(1, Number.parseInt(page, 10) || 1) + const pageSize = 15 const visitorToken = cookieStore.get("visitor_token")?.value if (!visitorToken) return notFound() - const features = await databaseDrizzle.query.feature.findMany({ - where: (f, ops) => ops.eq(f.projectId, projectId), - orderBy: (f, ops) => ops.desc(f.createdAt), - with: { - comments: { columns: { id: true } }, // only count for list - upvotes: { - where: (u, ops) => - ops.eq(u.voterToken, visitorToken), - columns: { id: true }, + const [features, totalCountResult] = await Promise.all([ + databaseDrizzle.query.feature.findMany({ + where: (f, ops) => ops.eq(f.projectId, projectId), + orderBy: (f, ops) => ops.desc(f.createdAt), + limit: pageSize, + offset: (currentPage - 1) * pageSize, + with: { + comments: { columns: { id: true } }, // only count for list + upvotes: { + where: (u, ops) => + ops.eq(u.voterToken, visitorToken), + columns: { id: true }, + }, + tags: { with: { tag: true } }, }, - tags: { with: { tag: true } }, - }, - }); + }), + databaseDrizzle + .select({ count: sql`count(*)` }) + .from(feature) + .where(eq(feature.projectId, projectId)) + .limit(1), + ]); + + const totalCount = totalCountResult[0]?.count ?? 0; + const totalPages = Math.ceil(totalCount / pageSize); return ( @@ -42,8 +58,8 @@ export default async function page({ params, searchParams }: { params: Promise<{
diff --git a/app/pub/[projectId]/roadmap/page.tsx b/app/pub/[projectId]/roadmap/page.tsx index 730ffbc..42fea3d 100644 --- a/app/pub/[projectId]/roadmap/page.tsx +++ b/app/pub/[projectId]/roadmap/page.tsx @@ -54,6 +54,19 @@ export default async function page({ params, searchParams }: { params: Promise<{ }, orderBy: (f, { asc }) => [asc(f.createdAt)], }); + const featuresByStatus = features.reduce>( + (acc, current) => { + acc[current.status].push(current); + return acc; + }, + { + under_review: [], + planned: [], + in_progress: [], + done: [], + closed: [], + } + ); return ( @@ -65,11 +78,11 @@ export default async function page({ params, searchParams }: { params: Promise<{ {status.label} - {features.length} + {featuresByStatus[status.value as FeatureStatus].length}
- {features.map((feature) => feature.status === status.value && ( + {featuresByStatus[status.value as FeatureStatus].map((feature) => (
@@ -127,7 +140,7 @@ export default async function page({ params, searchParams }: { params: Promise<{ ))} - {features.length === 0 && ( + {featuresByStatus[status.value as FeatureStatus].length === 0 && (
None
diff --git a/app/widget-preview/page.tsx b/app/widget-preview/page.tsx index 5e6da0b..cfa5862 100644 --- a/app/widget-preview/page.tsx +++ b/app/widget-preview/page.tsx @@ -1,4 +1,6 @@ +import Script from "next/script" + export default async function WidgetPreviewPage({ searchParams, }: { @@ -6,7 +8,15 @@ export default async function WidgetPreviewPage({ }) { const param = await searchParams - const config = param.config ? JSON.parse(decodeURIComponent(param.config)) : null + let config: { projectId?: string } | null = null + + if (param.config) { + try { + config = JSON.parse(decodeURIComponent(param.config)) + } catch { + config = null + } + } return ( <> @@ -86,14 +96,20 @@ export default async function WidgetPreviewPage({
- +