diff --git a/apps/web/src/app/[locale]/dashboard/(user)/meshes/[id]/invite/page.tsx b/apps/web/src/app/[locale]/dashboard/(user)/meshes/[id]/invite/page.tsx index 5b507c8..dbc1138 100644 --- a/apps/web/src/app/[locale]/dashboard/(user)/meshes/[id]/invite/page.tsx +++ b/apps/web/src/app/[locale]/dashboard/(user)/meshes/[id]/invite/page.tsx @@ -13,13 +13,33 @@ export const generateMetadata = getMetadata({ export default async function InvitePage({ params, + searchParams, }: { params: Promise<{ id: string }>; + searchParams: Promise<{ onboarding?: string }>; }) { const { id } = await params; + const { onboarding } = await searchParams; + const isOnboarding = onboarding === "1"; return ( <> + {isOnboarding && ( +
+

+ 🎉 Mesh created +

+

+ Now generate your first invite link to share with a teammate — or + use it yourself to join this mesh from another laptop. Your + teammate runs{" "} + + claudemesh join <link> + {" "} + in their terminal. +

+
+ )}
Invite teammate diff --git a/apps/web/src/app/[locale]/dashboard/(user)/meshes/new/page.tsx b/apps/web/src/app/[locale]/dashboard/(user)/meshes/new/page.tsx index df94daa..a9c0ed3 100644 --- a/apps/web/src/app/[locale]/dashboard/(user)/meshes/new/page.tsx +++ b/apps/web/src/app/[locale]/dashboard/(user)/meshes/new/page.tsx @@ -11,9 +11,29 @@ export const generateMetadata = getMetadata({ description: "Create a mesh.", }); -export default function NewMeshPage() { +export default async function NewMeshPage({ + searchParams, +}: { + searchParams: Promise<{ onboarding?: string }>; +}) { + const { onboarding } = await searchParams; + const isOnboarding = onboarding === "1"; + return ( <> + {isOnboarding && ( +
+

+ Welcome to claudemesh 👋 +

+

+ Create your first mesh in 10 seconds. A mesh is the space where + your Claude Code sessions talk to each other. You can invite + teammates, share context, and route messages — all end-to-end + encrypted. +

+
+ )}
New mesh @@ -23,7 +43,7 @@ export default function NewMeshPage() {
- +
); diff --git a/apps/web/src/app/[locale]/dashboard/(user)/page.tsx b/apps/web/src/app/[locale]/dashboard/(user)/page.tsx index 46c15b6..bf095da 100644 --- a/apps/web/src/app/[locale]/dashboard/(user)/page.tsx +++ b/apps/web/src/app/[locale]/dashboard/(user)/page.tsx @@ -1,66 +1,84 @@ -"use client"; +import Link from "next/link"; +import { redirect } from "next/navigation"; -import { useTranslation } from "@turbostarter/i18n"; -import { Card, CardContent, CardHeader, CardTitle } from "@turbostarter/ui-web/card"; -import { Icons } from "@turbostarter/ui-web/icons"; +import { getMyMeshesResponseSchema } from "@turbostarter/api/schema"; +import { handle } from "@turbostarter/api/utils"; +import { Badge } from "@turbostarter/ui-web/badge"; +import { buttonVariants } from "@turbostarter/ui-web/button"; -/** - * Dashboard Home Page - * - * Welcome page for authenticated users. - */ -export default function DashboardPage() { - const { t } = useTranslation("dashboard"); +import { pathsConfig } from "~/config/paths"; +import { api } from "~/lib/api/server"; +import { getMetadata } from "~/lib/metadata"; + +export const generateMetadata = getMetadata({ + title: "Dashboard", + description: "Your meshes.", +}); + +export default async function DashboardHomePage() { + const { data } = await handle(api.my.meshes.$get, { + schema: getMyMeshesResponseSchema, + })({ + query: { page: "1", perPage: "6", sort: JSON.stringify([]) }, + }); + + // First-time onboarding: 0-mesh user → bounce to create + if (data.length === 0) { + redirect(`${pathsConfig.dashboard.user.meshes.new}?onboarding=1`); + } return ( -
-
-
-

- {t("welcome.title", { defaultValue: "Welcome to your Dashboard" })} -

-

- {t("welcome.description", { defaultValue: "Get started by exploring the features below." })} -

-
- -
- - - {t("features.aiChat.title", { defaultValue: "AI Chat" })} - - - -

- {t("features.aiChat.description", { defaultValue: "Have a conversation with AI assistants" })} -

-
-
- - - - {t("features.imageGeneration.title", { defaultValue: "Image Generation" })} - - - -

- {t("features.imageGeneration.description", { defaultValue: "Create images with AI" })} -

-
-
- - - - {t("features.pdfAnalysis.title", { defaultValue: "PDF Analysis" })} - - - -

- {t("features.pdfAnalysis.description", { defaultValue: "Upload and analyze PDF documents" })} -

-
-
-
+
+
+

Your meshes

+

+ Open one to see its members, generate invites, or share it. +

+
+
+ {data.map((m) => ( + +
+
+

+ {m.name} +

+

+ {m.slug} +

+
+ + {m.isOwner ? "owner" : m.myRole} + +
+
+ + {m.tier} + + + {m.memberCount} {m.memberCount === 1 ? "member" : "members"} + +
+ + ))} +
+
+ + All meshes + + + New mesh +
); diff --git a/apps/web/src/modules/mesh/create-mesh-form.tsx b/apps/web/src/modules/mesh/create-mesh-form.tsx index c252172..2e1628e 100644 --- a/apps/web/src/modules/mesh/create-mesh-form.tsx +++ b/apps/web/src/modules/mesh/create-mesh-form.tsx @@ -40,7 +40,9 @@ const slugify = (s: string) => .replace(/^-+|-+$/g, "") .slice(0, 40); -export const CreateMeshForm = () => { +export const CreateMeshForm = ({ + onboarding = false, +}: { onboarding?: boolean } = {}) => { const router = useRouter(); const form = useForm({ resolver: zodResolver(createMyMeshInputSchema), @@ -70,7 +72,11 @@ export const CreateMeshForm = () => { form.setError("slug", { message: res.error }); return; } - router.push(pathsConfig.dashboard.user.meshes.mesh(res.id)); + router.push( + onboarding + ? `${pathsConfig.dashboard.user.meshes.invite(res.id)}?onboarding=1` + : pathsConfig.dashboard.user.meshes.mesh(res.id), + ); } catch (e) { form.setError("root", { message: e instanceof Error ? e.message : "Failed to create mesh.",