- Added `react-router` dependency for improved routing capabilities. - Updated `@elysiajs/eden` to version 1.4.8 for enhanced functionality. - Implemented app shell rewrite exclusions in `next.config.ts` to streamline routing. - Replaced `AppShell` component with `StaticAppShell` in various pages for consistent layout handling. - Refactored navigation logic to utilize `react-router` hooks for better navigation management. - Removed unnecessary components and code from onboarding and organization pages to simplify structure.
Unified · Split
| @@ -106,6 +106,7 @@ · 105 unmodified lines | ||
| 106 | 106 | "radix-ui": "^1.4.3", |
| 107 | 107 | "react": "19.2.3", |
| 108 | 108 | "react-dom": "19.2.3", |
| 109 | + "react-router": "^7.9.5", | |
| 109 | 110 | "shiki": "^3.22.0", |
| 110 | 111 | "sonner": "^2.0.7", |
| 111 | 112 | "streamdown": "^2.1.0", |
| @@ -317,7 +318,7 @@ · 205 unmodified lines | ||
| 317 | 318 | |
| 318 | 319 | "@effect/rpc": ["@effect/rpc@0.72.2", "", { "dependencies": { "msgpackr": "^1.11.4" }, "peerDependencies": { "@effect/platform": "^0.93.3", "effect": "^3.19.5" } }, "sha512-BmTXybXCOq96D2r9mvSW/YdiTQs5CStnd4II+lfVKrMr3pMNERKLZ2LG37Tfm4Sy3Q8ire6IVVKO/CN+VR0uQQ=="], |
| 319 | 320 | |
| 320 | − "@elysiajs/eden": ["@elysiajs/eden@1.4.6", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-Tsa4NwXEWg/u73vWiYZQ3L5/ecgZSxqiEjYwpS+4qBKXeTZqZKl2hcgHJSVBL+InEDMi35Xugct7qyAXE5oM4Q=="], | |
| 321 | + "@elysiajs/eden": ["@elysiajs/eden@1.4.8", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-a7oct2kFa49tH+GawZtSUCZR2rQgucNYgGLz8alXUqb4IrU3PASA0T4zXJw4MhdV1Xb6vyiR7p7kkqJcVjgbkA=="], | |
| 321 | 322 | |
| 322 | 323 | "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], |
| 323 | 324 | |
| @@ -2189,6 +2190,8 @@ · 1865 unmodified lines | ||
| 2189 | 2190 | |
| 2190 | 2191 | "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], |
| 2191 | 2192 | |
| 2193 | + "react-router": ["react-router@7.13.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw=="], | |
| 2194 | + | |
| 2192 | 2195 | "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], |
| 2193 | 2196 | |
| 2194 | 2197 | "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], |
| @@ -2269,6 +2272,8 @@ · 74 unmodified lines | ||
| 2269 | 2272 | |
| 2270 | 2273 | "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], |
| 2271 | 2274 | |
| 2275 | + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], | |
| 2276 | + | |
| 2272 | 2277 | "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], |
| 2273 | 2278 | |
| 2274 | 2279 | "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], |
| @@ -1,4 +1,17 @@ | ||
| 1 | 1 | import type { NextConfig } from "next"; |
| 2 | + | |
| 3 | +const appShellRewriteExclusions = [ | |
| 4 | + "api(?:/|$)", | |
| 5 | + "_next(?:/|$)", | |
| 6 | + "favicon\\.ico$", | |
| 7 | + "sign-in(?:/|$)", | |
| 8 | + "sign-up(?:/|$)", | |
| 9 | + "sign-out(?:/|$)", | |
| 10 | + "callback(?:/|$)", | |
| 11 | + "static-app-shell(?:/|$)", | |
| 12 | + ".*\\..*", | |
| 13 | +].join("|"); | |
| 14 | + | |
| 2 | 15 | const nextConfig: NextConfig = { |
| 3 | 16 | transpilePackages: ["@executor/contracts"], |
| 4 | 17 | env: { |
| @@ -8,6 +21,14 @@ const nextConfig: NextConfig = { · 3 unmodified lines | ||
| 8 | 21 | NEXT_PUBLIC_WORKOS_CLIENT_ID: process.env.WORKOS_CLIENT_ID, |
| 9 | 22 | NEXT_PUBLIC_STRIPE_PRICE_ID: process.env.STRIPE_PRICE_ID, |
| 10 | 23 | }, |
| 24 | + async rewrites() { | |
| 25 | + return [ | |
| 26 | + { | |
| 27 | + source: `/((?!${appShellRewriteExclusions}).*)`, | |
| 28 | + destination: "/static-app-shell", | |
| 29 | + }, | |
| 30 | + ]; | |
| 31 | + }, | |
| 11 | 32 | }; |
| 12 | 33 | |
| 13 | 34 | export default nextConfig; |
| @@ -28,6 +28,7 @@ · 27 unmodified lines | ||
| 28 | 28 | "radix-ui": "^1.4.3", |
| 29 | 29 | "react": "19.2.3", |
| 30 | 30 | "react-dom": "19.2.3", |
| 31 | + "react-router": "^7.9.5", | |
| 31 | 32 | "shiki": "^3.22.0", |
| 32 | 33 | "sonner": "^2.0.7", |
| 33 | 34 | "streamdown": "^2.1.0", |
| @@ -1,5 +1,5 @@ | ||
| 1 | −import { ApprovalsView } from "@/components/approvals-view"; | |
| 1 | +import { StaticAppShell } from "@/components/static-app-shell"; | |
| 2 | 2 | |
| 3 | 3 | export default function ApprovalsPage() { |
| 4 | − return <ApprovalsView />; | |
| 4 | + return <StaticAppShell />; | |
| 5 | 5 | } |
| @@ -2,7 +2,6 @@ import type { Metadata } from "next"; · 1 unmodified line | ||
| 2 | 2 | import { JetBrains_Mono, Inter } from "next/font/google"; |
| 3 | 3 | import { ThemeProvider } from "next-themes"; |
| 4 | 4 | import { Toaster } from "@/components/ui/sonner"; |
| 5 | −import { AppShell } from "@/components/app-shell"; | |
| 6 | 5 | import { AppConvexProvider } from "@/lib/convex-provider"; |
| 7 | 6 | import { QueryProvider } from "@/lib/query-provider"; |
| 8 | 7 | import { SessionProvider } from "@/lib/session-context"; |
| @@ -42,7 +41,7 @@ export default function RootLayout({ · 33 unmodified lines | ||
| 42 | 41 | <AppConvexProvider> |
| 43 | 42 | <QueryProvider> |
| 44 | 43 | <SessionProvider> |
| 45 | − <AppShell>{children}</AppShell> | |
| 44 | + {children} | |
| 46 | 45 | </SessionProvider> |
| 47 | 46 | </QueryProvider> |
| 48 | 47 | </AppConvexProvider> |
| @@ -1,137 +1,5 @@ | ||
| 1 | −"use client"; | |
| 2 | − | |
| 3 | −import { useState, type FormEvent } from "react"; | |
| 4 | −import Link from "next/link"; | |
| 5 | −import { useRouter } from "next/navigation"; | |
| 6 | −import { useMutation } from "convex/react"; | |
| 7 | −import { Building2 } from "lucide-react"; | |
| 8 | −import { Button } from "@/components/ui/button"; | |
| 9 | −import { | |
| 10 | − Card, | |
| 11 | − CardContent, | |
| 12 | − CardDescription, | |
| 13 | − CardFooter, | |
| 14 | − CardHeader, | |
| 15 | − CardTitle, | |
| 16 | −} from "@/components/ui/card"; | |
| 17 | −import { Input } from "@/components/ui/input"; | |
| 18 | −import { convexApi } from "@/lib/convex-api"; | |
| 19 | −import { useSession } from "@/lib/session-context"; | |
| 1 | +import { StaticAppShell } from "@/components/static-app-shell"; | |
| 20 | 2 | |
| 21 | 3 | export default function OnboardingPage() { |
| 22 | − const router = useRouter(); | |
| 23 | − const createOrganization = useMutation(convexApi.organizations.create); | |
| 24 | − const { | |
| 25 | − isSignedInToWorkos, | |
| 26 | − organizations, | |
| 27 | − organizationsLoading, | |
| 28 | − switchWorkspace, | |
| 29 | − } = useSession(); | |
| 30 | − | |
| 31 | − const [name, setName] = useState(""); | |
| 32 | − const [error, setError] = useState<string | null>(null); | |
| 33 | − const [creating, setCreating] = useState(false); | |
| 34 | − | |
| 35 | − const handleSubmit = async (event: FormEvent<HTMLFormElement>) => { | |
| 36 | − event.preventDefault(); | |
| 37 | − const trimmed = name.trim(); | |
| 38 | − if (trimmed.length < 2) { | |
| 39 | − setError("Organization name must be at least 2 characters."); | |
| 40 | − return; | |
| 41 | − } | |
| 42 | − | |
| 43 | − setCreating(true); | |
| 44 | − setError(null); | |
| 45 | − try { | |
| 46 | − const result = await createOrganization({ name: trimmed }); | |
| 47 | − switchWorkspace(result.workspace.id); | |
| 48 | − router.replace("/"); | |
| 49 | − } catch (cause) { | |
| 50 | − setError(cause instanceof Error ? cause.message : "Failed to create organization"); | |
| 51 | − } finally { | |
| 52 | − setCreating(false); | |
| 53 | − } | |
| 54 | − }; | |
| 55 | − | |
| 56 | − if (!isSignedInToWorkos) { | |
| 57 | − return ( | |
| 58 | − <div className="min-h-[70vh] grid place-items-center"> | |
| 59 | − <Card className="w-full max-w-xl border-border"> | |
| 60 | − <CardHeader> | |
| 61 | − <CardTitle>Sign in required</CardTitle> | |
| 62 | − <CardDescription> | |
| 63 | − You need to sign in before setting up an organization. | |
| 64 | − </CardDescription> | |
| 65 | − </CardHeader> | |
| 66 | − <CardFooter className="justify-between gap-2"> | |
| 67 | − <Button asChild variant="outline"> | |
| 68 | − <Link href="/">Back</Link> | |
| 69 | − </Button> | |
| 70 | − <Button asChild> | |
| 71 | − <Link href="/sign-in">Sign in</Link> | |
| 72 | − </Button> | |
| 73 | − </CardFooter> | |
| 74 | − </Card> | |
| 75 | − </div> | |
| 76 | − ); | |
| 77 | − } | |
| 78 | − | |
| 79 | − if (!organizationsLoading && organizations.length > 0) { | |
| 80 | − return ( | |
| 81 | − <div className="min-h-[70vh] grid place-items-center"> | |
| 82 | − <Card className="w-full max-w-xl border-border"> | |
| 83 | − <CardHeader> | |
| 84 | − <CardTitle>Organization ready</CardTitle> | |
| 85 | − <CardDescription> | |
| 86 | − Your account is already linked to an organization. | |
| 87 | − </CardDescription> | |
| 88 | − </CardHeader> | |
| 89 | − <CardFooter> | |
| 90 | − <Button asChild className="w-full"> | |
| 91 | − <Link href="/">Continue to dashboard</Link> | |
| 92 | − </Button> | |
| 93 | − </CardFooter> | |
| 94 | − </Card> | |
| 95 | − </div> | |
| 96 | − ); | |
| 97 | − } | |
| 98 | − | |
| 99 | − return ( | |
| 100 | − <div className="min-h-[80vh] grid place-items-center"> | |
| 101 | − <Card className="w-full max-w-xl border-border"> | |
| 102 | − <CardHeader> | |
| 103 | − <div className="mb-2 inline-flex h-9 w-9 items-center justify-center rounded-md border border-border bg-muted"> | |
| 104 | − <Building2 className="h-4 w-4 text-muted-foreground" /> | |
| 105 | − </div> | |
| 106 | − <CardTitle>Name your organization</CardTitle> | |
| 107 | − <CardDescription> | |
| 108 | − This is your team space. You can invite members and create more workspaces later. | |
| 109 | − </CardDescription> | |
| 110 | − </CardHeader> | |
| 111 | − <form onSubmit={handleSubmit}> | |
| 112 | − <CardContent className="space-y-2"> | |
| 113 | − <Input | |
| 114 | − value={name} | |
| 115 | − onChange={(event) => { | |
| 116 | − setError(null); | |
| 117 | − setName(event.target.value); | |
| 118 | − }} | |
| 119 | − placeholder="Acme" | |
| 120 | − maxLength={64} | |
| 121 | − autoFocus | |
| 122 | − /> | |
| 123 | − {error ? <p className="text-xs text-destructive">{error}</p> : null} | |
| 124 | − </CardContent> | |
| 125 | − <CardFooter className="justify-end gap-2"> | |
| 126 | − <Button asChild variant="outline" disabled={creating}> | |
| 127 | − <Link href="/sign-out">Sign out</Link> | |
| 128 | − </Button> | |
| 129 | − <Button type="submit" disabled={creating}> | |
| 130 | − {creating ? "Creating..." : "Continue"} | |
| 131 | − </Button> | |
| 132 | − </CardFooter> | |
| 133 | − </form> | |
| 134 | − </Card> | |
| 135 | − </div> | |
| 136 | − ); | |
| 4 | + return <StaticAppShell />; | |
| 137 | 5 | } |
| @@ -1,10 +1,5 @@ | ||
| 1 | −import { Suspense } from "react"; | |
| 2 | −import { OrganizationSettingsView } from "@/components/organization-settings-view"; | |
| 1 | +import { StaticAppShell } from "@/components/static-app-shell"; | |
| 3 | 2 | |
| 4 | 3 | export default function OrganizationPage() { |
| 5 | − return ( | |
| 6 | − <Suspense> | |
| 7 | − <OrganizationSettingsView /> | |
| 8 | − </Suspense> | |
| 9 | − ); | |
| 4 | + return <StaticAppShell />; | |
| 10 | 5 | } |
| @@ -1,5 +1,5 @@ | ||
| 1 | −import { DashboardView } from "@/components/dashboard-view"; | |
| 1 | +import { StaticAppShell } from "@/components/static-app-shell"; | |
| 2 | 2 | |
| 3 | 3 | export default function DashboardPage() { |
| 4 | − return <DashboardView />; | |
| 4 | + return <StaticAppShell />; | |
| 5 | 5 | } |
| @@ -0,0 +1,5 @@ | ||
| 1 | +import { StaticAppShell } from "@/components/static-app-shell"; | |
| 2 | + | |
| 3 | +export default function StaticAppShellPage() { | |
| 4 | + return <StaticAppShell />; | |
| 5 | +} | |
| @@ -1,10 +1,5 @@ | ||
| 1 | −import { Suspense } from "react"; | |
| 2 | −import { TasksView } from "@/components/tasks-view"; | |
| 1 | +import { StaticAppShell } from "@/components/static-app-shell"; | |
| 3 | 2 | |
| 4 | 3 | export default function TasksPage() { |
| 5 | − return ( | |
| 6 | − <Suspense> | |
| 7 | − <TasksView /> | |
| 8 | − </Suspense> | |
| 9 | − ); | |
| 4 | + return <StaticAppShell />; | |
| 10 | 5 | } |
| @@ -1,16 +1,5 @@ | ||
| 1 | −import { ToolsView } from "@/components/tools-view"; | |
| 1 | +import { StaticAppShell } from "@/components/static-app-shell"; | |
| 2 | 2 | |
| 3 | −export default async function ToolsPage({ | |
| 4 | − searchParams, | |
| 5 | −}: { | |
| 6 | − searchParams: Promise<Record<string, string | string[] | undefined>>; | |
| 7 | −}) { | |
| 8 | − const params = await searchParams; | |
| 9 | − const source = typeof params.source === "string" ? params.source : null; | |
| 10 | − | |
| 11 | − return ( | |
| 12 | − <div className="h-full min-h-0"> | |
| 13 | − <ToolsView initialSource={source} /> | |
| 14 | − </div> | |
| 15 | − ); | |
| 3 | +export default function ToolsPage() { | |
| 4 | + return <StaticAppShell />; | |
| 16 | 5 | } |
| @@ -1,8 +1,7 @@ | ||
| 1 | 1 | "use client"; |
| 2 | 2 | |
| 3 | 3 | import { Suspense, useEffect, useMemo, useState, type FormEvent } from "react"; |
| 4 | −import Link from "next/link"; | |
| 5 | −import { usePathname, useRouter, useSearchParams } from "next/navigation"; | |
| 4 | +import { Link, useLocation, useNavigate, useSearchParams } from "react-router"; | |
| 6 | 5 | import { useMutation } from "convex/react"; |
| 7 | 6 | import { |
| 8 | 7 | LayoutDashboard, |
| @@ -38,7 +37,7 @@ import { · 29 unmodified lines | ||
| 38 | 37 | import { Input } from "@/components/ui/input"; |
| 39 | 38 | import { useSession } from "@/lib/session-context"; |
| 40 | 39 | import { convexApi } from "@/lib/convex-api"; |
| 41 | −import { workosEnabled } from "@/lib/auth-capabilities"; | |
| 40 | +import { anonymousDemoEnabled, workosEnabled } from "@/lib/auth-capabilities"; | |
| 42 | 41 | import { ThemeSwitcher } from "@/components/theme-switcher"; |
| 43 | 42 | import { ApprovalNotifier } from "@/components/approval-notifier"; |
| 44 | 43 | import { cn } from "@/lib/utils"; |
| @@ -51,7 +50,8 @@ const NAV_ITEMS = [ · 6 unmodified lines | ||
| 51 | 50 | ]; |
| 52 | 51 | |
| 53 | 52 | function NavLinks({ onClick }: { onClick?: () => void }) { |
| 54 | − const pathname = usePathname(); | |
| 53 | + const location = useLocation(); | |
| 54 | + const pathname = location.pathname; | |
| 55 | 55 | |
| 56 | 56 | return ( |
| 57 | 57 | <nav className="flex flex-col gap-1"> |
| @@ -63,7 +63,7 @@ function NavLinks({ onClick }: { onClick?: () => void }) { · 5 unmodified lines | ||
| 63 | 63 | return ( |
| 64 | 64 | <Link |
| 65 | 65 | key={item.href} |
| 66 | − href={item.href} | |
| 66 | + to={item.href} | |
| 67 | 67 | onClick={onClick} |
| 68 | 68 | className={cn( |
| 69 | 69 | "flex items-center gap-3 px-3 py-2.5 rounded-md text-sm font-medium transition-colors", |
| @@ -82,12 +82,11 @@ function NavLinks({ onClick }: { onClick?: () => void }) { · 12 unmodified lines | ||
| 82 | 82 | } |
| 83 | 83 | |
| 84 | 84 | function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) { |
| 85 | − const router = useRouter(); | |
| 85 | + const navigate = useNavigate(); | |
| 86 | 86 | const createOrganizationMutation = useMutation(convexApi.organizations.create); |
| 87 | 87 | const { |
| 88 | 88 | context, |
| 89 | 89 | mode, |
| 90 | − clientConfig, | |
| 91 | 90 | workspaces, |
| 92 | 91 | switchWorkspace, |
| 93 | 92 | creatingWorkspace, |
| @@ -261,7 +260,7 @@ function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) { · 167 unmodified lines | ||
| 261 | 260 | className="inline-flex h-5 w-5 items-center justify-center rounded-sm border border-border text-muted-foreground hover:bg-accent hover:text-foreground" |
| 262 | 261 | onClick={() => { |
| 263 | 262 | switchWorkspace(settingsWorkspace.id); |
| 264 | − router.push("/organization?tab=members"); | |
| 263 | + navigate("/organization?tab=members"); | |
| 265 | 264 | }} |
| 266 | 265 | aria-label={`Open ${group.organizationName} settings`} |
| 267 | 266 | title="Organization settings" |
| @@ -402,14 +401,13 @@ function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) { · 134 unmodified lines | ||
| 402 | 401 | function SessionInfo() { |
| 403 | 402 | const { |
| 404 | 403 | loading, |
| 405 | − clientConfig, | |
| 406 | 404 | isSignedInToWorkos, |
| 407 | 405 | workosProfile, |
| 408 | 406 | context, |
| 409 | 407 | workspaces, |
| 410 | 408 | resetWorkspace, |
| 411 | 409 | } = useSession(); |
| 412 | − const searchParams = useSearchParams(); | |
| 410 | + const [searchParams] = useSearchParams(); | |
| 413 | 411 | const deleteCurrentAccountMutation = useMutation(convexApi.accounts.deleteCurrentAccount); |
| 414 | 412 | const [accountSettingsOpen, setAccountSettingsOpen] = useState(false); |
| 415 | 413 | const [deleteConfirmText, setDeleteConfirmText] = useState(""); |
| @@ -523,7 +521,7 @@ function SessionInfo() { · 107 unmodified lines | ||
| 523 | 521 | Account Settings |
| 524 | 522 | </DropdownMenuItem> |
| 525 | 523 | <DropdownMenuItem asChild className="text-xs"> |
| 526 | − <Link href="/sign-out">Sign out</Link> | |
| 524 | + <Link to="/sign-out" reloadDocument>Sign out</Link> | |
| 527 | 525 | </DropdownMenuItem> |
| 528 | 526 | </DropdownMenuContent> |
| 529 | 527 | </DropdownMenu> |
| @@ -587,7 +585,7 @@ function SessionInfo() { · 57 unmodified lines | ||
| 587 | 585 | ) : ( |
| 588 | 586 | <div className="px-3 py-3 space-y-2"> |
| 589 | 587 | {workosEnabled ? ( |
| 590 | − <Link href={signInHref} className="block"> | |
| 588 | + <Link to={signInHref} reloadDocument className="block"> | |
| 591 | 589 | <Button |
| 592 | 590 | variant="outline" |
| 593 | 591 | className="w-full h-9 justify-center gap-2 text-xs font-medium" |
| @@ -663,9 +661,79 @@ function MobileHeader() { · 69 unmodified lines | ||
| 663 | 661 | ); |
| 664 | 662 | } |
| 665 | 663 | |
| 664 | +function NoOrganizationModal({ enabled }: { enabled: boolean }) { | |
| 665 | + const { | |
| 666 | + loading, | |
| 667 | + organizations, | |
| 668 | + organizationsLoading, | |
| 669 | + context, | |
| 670 | + isSignedInToWorkos, | |
| 671 | + createAnonymousWorkspace, | |
| 672 | + creatingAnonymousWorkspace, | |
| 673 | + } = useSession(); | |
| 674 | + const [error, setError] = useState<string | null>(null); | |
| 675 | + | |
| 676 | + const shouldShow = enabled | |
| 677 | + && !loading | |
| 678 | + && !organizationsLoading | |
| 679 | + && !context | |
| 680 | + && !isSignedInToWorkos | |
| 681 | + && organizations.length === 0; | |
| 682 | + | |
| 683 | + const handleCreateAnonymousWorkspace = async () => { | |
| 684 | + setError(null); | |
| 685 | + try { | |
| 686 | + await createAnonymousWorkspace(); | |
| 687 | + } catch (cause) { | |
| 688 | + const message = cause instanceof Error ? cause.message : "Failed to create anonymous workspace"; | |
| 689 | + setError(message); | |
| 690 | + } | |
| 691 | + }; | |
| 692 | + | |
| 693 | + return ( | |
| 694 | + <Dialog open={shouldShow}> | |
| 695 | + <DialogContent | |
| 696 | + showCloseButton={false} | |
| 697 | + onEscapeKeyDown={(event) => event.preventDefault()} | |
| 698 | + onInteractOutside={(event) => event.preventDefault()} | |
| 699 | + > | |
| 700 | + <DialogHeader> | |
| 701 | + <DialogTitle>Choose how to continue</DialogTitle> | |
| 702 | + <DialogDescription> | |
| 703 | + Sign in to access your organizations, or create an anonymous workspace to continue as a guest. | |
| 704 | + </DialogDescription> | |
| 705 | + </DialogHeader> | |
| 706 | + <div className="space-y-2"> | |
| 707 | + {workosEnabled ? ( | |
| 708 | + <Button asChild className="w-full" disabled={creatingAnonymousWorkspace}> | |
| 709 | + <Link to="/sign-in" reloadDocument className="gap-2"> | |
| 710 | + <LogIn className="h-4 w-4" /> | |
| 711 | + Sign in | |
| 712 | + </Link> | |
| 713 | + </Button> | |
| 714 | + ) : null} | |
| 715 | + <Button | |
| 716 | + variant="outline" | |
| 717 | + className="w-full" | |
| 718 | + onClick={handleCreateAnonymousWorkspace} | |
| 719 | + disabled={creatingAnonymousWorkspace || !anonymousDemoEnabled} | |
| 720 | + > | |
| 721 | + {creatingAnonymousWorkspace ? "Creating anonymous workspace..." : "Create anonymous workspace"} | |
| 722 | + </Button> | |
| 723 | + {!anonymousDemoEnabled ? ( | |
| 724 | + <p className="text-xs text-muted-foreground">Anonymous workspace creation is disabled.</p> | |
| 725 | + ) : null} | |
| 726 | + {error ? <p className="text-xs text-destructive">{error}</p> : null} | |
| 727 | + </div> | |
| 728 | + </DialogContent> | |
| 729 | + </Dialog> | |
| 730 | + ); | |
| 731 | +} | |
| 732 | + | |
| 666 | 733 | export function AppShell({ children }: { children: React.ReactNode }) { |
| 667 | − const pathname = usePathname(); | |
| 668 | − const router = useRouter(); | |
| 734 | + const location = useLocation(); | |
| 735 | + const pathname = location.pathname; | |
| 736 | + const navigate = useNavigate(); | |
| 669 | 737 | const { loading, organizations, organizationsLoading, isSignedInToWorkos } = useSession(); |
| 670 | 738 | |
| 671 | 739 | const onOnboardingRoute = pathname.startsWith("/onboarding"); |
| @@ -677,12 +745,12 @@ export function AppShell({ children }: { children: React.ReactNode }) { · 5 unmodified lines | ||
| 677 | 745 | } |
| 678 | 746 | |
| 679 | 747 | if (needsOnboarding && !onOnboardingRoute) { |
| 680 | − router.replace("/onboarding"); | |
| 748 | + navigate("/onboarding", { replace: true }); | |
| 681 | 749 | return; |
| 682 | 750 | } |
| 683 | 751 | |
| 684 | 752 | if (!needsOnboarding && onOnboardingRoute && organizations.length > 0) { |
| 685 | − router.replace("/"); | |
| 753 | + navigate("/", { replace: true }); | |
| 686 | 754 | } |
| 687 | 755 | }, [ |
| 688 | 756 | loading, |
| @@ -690,7 +758,7 @@ export function AppShell({ children }: { children: React.ReactNode }) { · 1 unmodified line | ||
| 690 | 758 | needsOnboarding, |
| 691 | 759 | onOnboardingRoute, |
| 692 | 760 | organizations.length, |
| 693 | − router, | |
| 761 | + navigate, | |
| 694 | 762 | ]); |
| 695 | 763 | |
| 696 | 764 | if (isSignedInToWorkos && organizationsLoading && !onOnboardingRoute) { |
| @@ -712,6 +780,7 @@ export function AppShell({ children }: { children: React.ReactNode }) { · 15 unmodified lines | ||
| 712 | 780 | return ( |
| 713 | 781 | <div className="flex min-h-screen"> |
| 714 | 782 | <ApprovalNotifier /> |
| 783 | + <NoOrganizationModal enabled /> | |
| 715 | 784 | <Sidebar /> |
| 716 | 785 | <div className="flex-1 flex flex-col min-w-0"> |
| 717 | 786 | <MobileHeader /> |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | "use client"; |
| 2 | 2 | |
| 3 | 3 | import { useEffect, useRef } from "react"; |
| 4 | −import { useRouter } from "next/navigation"; | |
| 4 | +import { useNavigate } from "react-router"; | |
| 5 | 5 | import { useQuery } from "convex/react"; |
| 6 | 6 | import { toast } from "sonner"; |
| 7 | 7 | import { useSession } from "@/lib/session-context"; |
| @@ -9,7 +9,7 @@ import { convexApi } from "@/lib/convex-api"; · 1 unmodified line | ||
| 9 | 9 | import type { PendingApprovalRecord } from "@/lib/types"; |
| 10 | 10 | |
| 11 | 11 | export function ApprovalNotifier() { |
| 12 | − const router = useRouter(); | |
| 12 | + const navigate = useNavigate(); | |
| 13 | 13 | const { context } = useSession(); |
| 14 | 14 | const seenApprovalIdsRef = useRef<Set<string>>(new Set()); |
| 15 | 15 | const activeWorkspaceRef = useRef<string | null>(null); |
| @@ -58,12 +58,12 @@ export function ApprovalNotifier() { · 42 unmodified lines | ||
| 58 | 58 | : "Open approvals to review pending tool calls.", |
| 59 | 59 | action: { |
| 60 | 60 | label: "Review", |
| 61 | − onClick: () => router.push("/approvals"), | |
| 61 | + onClick: () => navigate("/approvals"), | |
| 62 | 62 | }, |
| 63 | 63 | }, |
| 64 | 64 | ); |
| 65 | 65 | |
| 66 | − }, [approvals, context, router]); | |
| 66 | + }, [approvals, context, navigate]); | |
| 67 | 67 | |
| 68 | 68 | return null; |
| 69 | 69 | } |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | "use client"; |
| 2 | 2 | |
| 3 | 3 | import { useMemo, useState } from "react"; |
| 4 | −import { useSearchParams } from "next/navigation"; | |
| 4 | +import { useSearchParams } from "react-router"; | |
| 5 | 5 | import { useAction, useMutation, useQuery } from "convex/react"; |
| 6 | 6 | import { AlertTriangle, BadgeCheck, CreditCard, RefreshCcw } from "lucide-react"; |
| 7 | 7 | import { PageHeader } from "@/components/page-header"; |
| @@ -50,7 +50,7 @@ function formatTimestamp(value: number | null | undefined): string { · 42 unmodified lines | ||
| 50 | 50 | |
| 51 | 51 | export function BillingView({ showHeader = true }: BillingViewProps) { |
| 52 | 52 | const { context, organizations, organizationsLoading, workspaces } = useSession(); |
| 53 | − const searchParams = useSearchParams(); | |
| 53 | + const [searchParams] = useSearchParams(); | |
| 54 | 54 | const [actionState, setActionState] = useState<"idle" | "running" | "success" | "error">("idle"); |
| 55 | 55 | const [actionMessage, setActionMessage] = useState<string | null>(null); |
| 56 | 56 | const [priceId, setPriceId] = useState(() => process.env.NEXT_PUBLIC_STRIPE_PRICE_ID ?? ""); |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | "use client"; |
| 2 | 2 | |
| 3 | 3 | import { useMemo } from "react"; |
| 4 | −import { useRouter } from "next/navigation"; | |
| 4 | +import { useNavigate } from "react-router"; | |
| 5 | 5 | import { |
| 6 | 6 | Play, |
| 7 | 7 | ShieldCheck, |
| @@ -67,10 +67,10 @@ function StatCard({ · 59 unmodified lines | ||
| 67 | 67 | } |
| 68 | 68 | |
| 69 | 69 | function PendingApprovalRow({ approval }: { approval: PendingApprovalRecord }) { |
| 70 | − const router = useRouter(); | |
| 70 | + const navigate = useNavigate(); | |
| 71 | 71 | return ( |
| 72 | 72 | <button |
| 73 | − onClick={() => router.push("/approvals")} | |
| 73 | + onClick={() => navigate("/approvals")} | |
| 74 | 74 | className="w-full flex items-center gap-3 px-3 py-2.5 rounded-md hover:bg-accent/50 transition-colors text-left group" |
| 75 | 75 | > |
| 76 | 76 | <div className="h-2 w-2 rounded-full bg-terminal-amber pulse-dot shrink-0" /> |
| @@ -88,10 +88,10 @@ function PendingApprovalRow({ approval }: { approval: PendingApprovalRecord }) { · 11 unmodified lines | ||
| 88 | 88 | } |
| 89 | 89 | |
| 90 | 90 | function RecentTaskRow({ task }: { task: TaskRecord }) { |
| 91 | − const router = useRouter(); | |
| 91 | + const navigate = useNavigate(); | |
| 92 | 92 | return ( |
| 93 | 93 | <button |
| 94 | − onClick={() => router.push(`/tasks?selected=${task.id}`)} | |
| 94 | + onClick={() => navigate(`/tasks?selected=${task.id}`)} | |
| 95 | 95 | className="w-full flex items-center gap-3 px-3 py-2.5 rounded-md hover:bg-accent/50 transition-colors text-left group" |
| 96 | 96 | > |
| 97 | 97 | <div className="flex-1 min-w-0"> |
| @@ -122,7 +122,7 @@ function sourceType(source?: string): string { · 24 unmodified lines | ||
| 122 | 122 | } |
| 123 | 123 | |
| 124 | 124 | function ToolsSummaryCard({ tools }: { tools: ToolDescriptor[] }) { |
| 125 | − const router = useRouter(); | |
| 125 | + const navigate = useNavigate(); | |
| 126 | 126 | |
| 127 | 127 | const groups = useMemo(() => { |
| 128 | 128 | const map = new Map< |
| @@ -180,7 +180,7 @@ function ToolsSummaryCard({ tools }: { tools: ToolDescriptor[] }) { · 51 unmodified lines | ||
| 180 | 180 | variant="ghost" |
| 181 | 181 | size="sm" |
| 182 | 182 | className="text-xs h-7" |
| 183 | − onClick={() => router.push("/tools")} | |
| 183 | + onClick={() => navigate("/tools")} | |
| 184 | 184 | > |
| 185 | 185 | Manage |
| 186 | 186 | <ArrowRight className="h-3 w-3 ml-1" /> |
| @@ -194,7 +194,7 @@ function ToolsSummaryCard({ tools }: { tools: ToolDescriptor[] }) { · 7 unmodified lines | ||
| 194 | 194 | return ( |
| 195 | 195 | <button |
| 196 | 196 | key={group.name} |
| 197 | − onClick={() => router.push(`/tools?source=${encodeURIComponent(group.name)}`)} | |
| 197 | + onClick={() => navigate(`/tools?source=${encodeURIComponent(group.name)}`)} | |
| 198 | 198 | className="flex items-center gap-3 px-3 py-2 rounded-md hover:bg-accent/40 transition-colors text-left group/row w-full" |
| 199 | 199 | > |
| 200 | 200 | <div className="h-7 w-7 rounded bg-muted flex items-center justify-center shrink-0"> |
| @@ -0,0 +1,136 @@ | ||
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import { useState, type FormEvent } from "react"; | |
| 4 | +import { Link, useNavigate } from "react-router"; | |
| 5 | +import { useMutation } from "convex/react"; | |
| 6 | +import { Building2 } from "lucide-react"; | |
| 7 | +import { Button } from "@/components/ui/button"; | |
| 8 | +import { | |
| 9 | + Card, | |
| 10 | + CardContent, | |
| 11 | + CardDescription, | |
| 12 | + CardFooter, | |
| 13 | + CardHeader, | |
| 14 | + CardTitle, | |
| 15 | +} from "@/components/ui/card"; | |
| 16 | +import { Input } from "@/components/ui/input"; | |
| 17 | +import { convexApi } from "@/lib/convex-api"; | |
| 18 | +import { useSession } from "@/lib/session-context"; | |
| 19 | + | |
| 20 | +export function OnboardingView() { | |
| 21 | + const navigate = useNavigate(); | |
| 22 | + const createOrganization = useMutation(convexApi.organizations.create); | |
| 23 | + const { | |
| 24 | + isSignedInToWorkos, | |
| 25 | + organizations, | |
| 26 | + organizationsLoading, | |
| 27 | + switchWorkspace, | |
| 28 | + } = useSession(); | |
| 29 | + | |
| 30 | + const [name, setName] = useState(""); | |
| 31 | + const [error, setError] = useState<string | null>(null); | |
| 32 | + const [creating, setCreating] = useState(false); | |
| 33 | + | |
| 34 | + const handleSubmit = async (event: FormEvent<HTMLFormElement>) => { | |
| 35 | + event.preventDefault(); | |
| 36 | + const trimmed = name.trim(); | |
| 37 | + if (trimmed.length < 2) { | |
| 38 | + setError("Organization name must be at least 2 characters."); | |
| 39 | + return; | |
| 40 | + } | |
| 41 | + | |
| 42 | + setCreating(true); | |
| 43 | + setError(null); | |
| 44 | + try { | |
| 45 | + const result = await createOrganization({ name: trimmed }); | |
| 46 | + switchWorkspace(result.workspace.id); | |
| 47 | + navigate("/", { replace: true }); | |
| 48 | + } catch (cause) { | |
| 49 | + setError(cause instanceof Error ? cause.message : "Failed to create organization"); | |
| 50 | + } finally { | |
| 51 | + setCreating(false); | |
| 52 | + } | |
| 53 | + }; | |
| 54 | + | |
| 55 | + if (!isSignedInToWorkos) { | |
| 56 | + return ( | |
| 57 | + <div className="min-h-[70vh] grid place-items-center"> | |
| 58 | + <Card className="w-full max-w-xl border-border"> | |
| 59 | + <CardHeader> | |
| 60 | + <CardTitle>Sign in required</CardTitle> | |
| 61 | + <CardDescription> | |
| 62 | + You need to sign in before setting up an organization. | |
| 63 | + </CardDescription> | |
| 64 | + </CardHeader> | |
| 65 | + <CardFooter className="justify-between gap-2"> | |
| 66 | + <Button asChild variant="outline"> | |
| 67 | + <Link to="/">Back</Link> | |
| 68 | + </Button> | |
| 69 | + <Button asChild> | |
| 70 | + <Link to="/sign-in" reloadDocument>Sign in</Link> | |
| 71 | + </Button> | |
| 72 | + </CardFooter> | |
| 73 | + </Card> | |
| 74 | + </div> | |
| 75 | + ); | |
| 76 | + } | |
| 77 | + | |
| 78 | + if (!organizationsLoading && organizations.length > 0) { | |
| 79 | + return ( | |
| 80 | + <div className="min-h-[70vh] grid place-items-center"> | |
| 81 | + <Card className="w-full max-w-xl border-border"> | |
| 82 | + <CardHeader> | |
| 83 | + <CardTitle>Organization ready</CardTitle> | |
| 84 | + <CardDescription> | |
| 85 | + Your account is already linked to an organization. | |
| 86 | + </CardDescription> | |
| 87 | + </CardHeader> | |
| 88 | + <CardFooter> | |
| 89 | + <Button asChild className="w-full"> | |
| 90 | + <Link to="/">Continue to dashboard</Link> | |
| 91 | + </Button> | |
| 92 | + </CardFooter> | |
| 93 | + </Card> | |
| 94 | + </div> | |
| 95 | + ); | |
| 96 | + } | |
| 97 | + | |
| 98 | + return ( | |
| 99 | + <div className="min-h-[80vh] grid place-items-center"> | |
| 100 | + <Card className="w-full max-w-xl border-border"> | |
| 101 | + <CardHeader> | |
| 102 | + <div className="mb-2 inline-flex h-9 w-9 items-center justify-center rounded-md border border-border bg-muted"> | |
| 103 | + <Building2 className="h-4 w-4 text-muted-foreground" /> | |
| 104 | + </div> | |
| 105 | + <CardTitle>Name your organization</CardTitle> | |
| 106 | + <CardDescription> | |
| 107 | + This is your team space. You can invite members and create more workspaces later. | |
| 108 | + </CardDescription> | |
| 109 | + </CardHeader> | |
| 110 | + <form onSubmit={handleSubmit}> | |
| 111 | + <CardContent className="space-y-2"> | |
| 112 | + <Input | |
| 113 | + value={name} | |
| 114 | + onChange={(event) => { | |
| 115 | + setError(null); | |
| 116 | + setName(event.target.value); | |
| 117 | + }} | |
| 118 | + placeholder="Acme" | |
| 119 | + maxLength={64} | |
| 120 | + autoFocus | |
| 121 | + /> | |
| 122 | + {error ? <p className="text-xs text-destructive">{error}</p> : null} | |
| 123 | + </CardContent> | |
| 124 | + <CardFooter className="justify-end gap-2"> | |
| 125 | + <Button asChild variant="outline" disabled={creating}> | |
| 126 | + <Link to="/sign-out" reloadDocument>Sign out</Link> | |
| 127 | + </Button> | |
| 128 | + <Button type="submit" disabled={creating}> | |
| 129 | + {creating ? "Creating..." : "Continue"} | |
| 130 | + </Button> | |
| 131 | + </CardFooter> | |
| 132 | + </form> | |
| 133 | + </Card> | |
| 134 | + </div> | |
| 135 | + ); | |
| 136 | +} | |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | "use client"; |
| 2 | 2 | |
| 3 | 3 | import { useMemo } from "react"; |
| 4 | −import { usePathname, useRouter, useSearchParams } from "next/navigation"; | |
| 4 | +import { useLocation, useNavigate, useSearchParams } from "react-router"; | |
| 5 | 5 | import { CreditCard, Users } from "lucide-react"; |
| 6 | 6 | import { BillingView } from "@/components/billing-view"; |
| 7 | 7 | import { MembersView } from "@/components/members-view"; |
| @@ -15,9 +15,10 @@ function normalizeTab(value: string | null): OrganizationTab { · 7 unmodified lines | ||
| 15 | 15 | } |
| 16 | 16 | |
| 17 | 17 | export function OrganizationSettingsView() { |
| 18 | − const router = useRouter(); | |
| 19 | − const pathname = usePathname(); | |
| 20 | − const searchParams = useSearchParams(); | |
| 18 | + const navigate = useNavigate(); | |
| 19 | + const location = useLocation(); | |
| 20 | + const pathname = location.pathname; | |
| 21 | + const [searchParams] = useSearchParams(); | |
| 21 | 22 | |
| 22 | 23 | const tab = useMemo(() => normalizeTab(searchParams.get("tab")), [searchParams]); |
| 23 | 24 | |
| @@ -25,7 +26,7 @@ export function OrganizationSettingsView() { · 1 unmodified line | ||
| 25 | 26 | const normalizedTab = normalizeTab(nextTab); |
| 26 | 27 | const nextSearch = new URLSearchParams(searchParams.toString()); |
| 27 | 28 | nextSearch.set("tab", normalizedTab); |
| 28 | − router.replace(`${pathname}?${nextSearch.toString()}`); | |
| 29 | + navigate(`${pathname}?${nextSearch.toString()}`, { replace: true }); | |
| 29 | 30 | }; |
| 30 | 31 | |
| 31 | 32 | return ( |
| @@ -0,0 +1,11 @@ | ||
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import dynamic from "next/dynamic"; | |
| 4 | + | |
| 5 | +const FrontendApp = dynamic(() => import("@/frontend/app"), { | |
| 6 | + ssr: false, | |
| 7 | +}); | |
| 8 | + | |
| 9 | +export function StaticAppShell() { | |
| 10 | + return <FrontendApp />; | |
| 11 | +} | |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | "use client"; |
| 2 | 2 | |
| 3 | 3 | import { useState, useCallback, useMemo } from "react"; |
| 4 | −import { useSearchParams, useRouter } from "next/navigation"; | |
| 4 | +import { useNavigate, useSearchParams } from "react-router"; | |
| 5 | 5 | import { |
| 6 | 6 | Play, |
| 7 | 7 | ChevronRight, |
| @@ -361,8 +361,8 @@ function TaskDetail({ · 353 unmodified lines | ||
| 361 | 361 | |
| 362 | 362 | export function TasksView() { |
| 363 | 363 | const { context, loading: sessionLoading } = useSession(); |
| 364 | − const router = useRouter(); | |
| 365 | − const searchParams = useSearchParams(); | |
| 364 | + const navigate = useNavigate(); | |
| 365 | + const [searchParams] = useSearchParams(); | |
| 366 | 366 | const selectedId = searchParams.get("selected"); |
| 367 | 367 | |
| 368 | 368 | const tasks = useQuery( |
| @@ -377,12 +377,12 @@ export function TasksView() { · 8 unmodified lines | ||
| 377 | 377 | const selectTask = useCallback( |
| 378 | 378 | (taskId: string | null) => { |
| 379 | 379 | if (taskId) { |
| 380 | − router.push(`/tasks?selected=${taskId}`, { scroll: false }); | |
| 380 | + navigate(`/tasks?selected=${taskId}`); | |
| 381 | 381 | } else { |
| 382 | − router.push("/tasks", { scroll: false }); | |
| 382 | + navigate("/tasks"); | |
| 383 | 383 | } |
| 384 | 384 | }, |
| 385 | − [router], | |
| 385 | + [navigate], | |
| 386 | 386 | ); |
| 387 | 387 | |
| 388 | 388 | if (sessionLoading) { |
| @@ -0,0 +1,50 @@ | ||
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import { Navigate, Outlet, Route, Routes, BrowserRouter, useSearchParams } from "react-router"; | |
| 4 | +import { AppShell } from "@/components/app-shell"; | |
| 5 | +import { ApprovalsView } from "@/components/approvals-view"; | |
| 6 | +import { DashboardView } from "@/components/dashboard-view"; | |
| 7 | +import { OnboardingView } from "@/components/onboarding-view"; | |
| 8 | +import { OrganizationSettingsView } from "@/components/organization-settings-view"; | |
| 9 | +import { TasksView } from "@/components/tasks-view"; | |
| 10 | +import { ToolsView } from "@/components/tools-view"; | |
| 11 | + | |
| 12 | +function ToolsRoute() { | |
| 13 | + const [searchParams] = useSearchParams(); | |
| 14 | + const source = searchParams.get("source"); | |
| 15 | + | |
| 16 | + return ( | |
| 17 | + <div className="h-full min-h-0"> | |
| 18 | + <ToolsView initialSource={source} /> | |
| 19 | + </div> | |
| 20 | + ); | |
| 21 | +} | |
| 22 | + | |
| 23 | +function ShellLayout() { | |
| 24 | + return ( | |
| 25 | + <AppShell> | |
| 26 | + <Outlet /> | |
| 27 | + </AppShell> | |
| 28 | + ); | |
| 29 | +} | |
| 30 | + | |
| 31 | +export default function FrontendApp() { | |
| 32 | + return ( | |
| 33 | + <BrowserRouter> | |
| 34 | + <Routes> | |
| 35 | + <Route element={<ShellLayout />}> | |
| 36 | + <Route path="/" element={<DashboardView />} /> | |
| 37 | + <Route path="/static-app-shell" element={<DashboardView />} /> | |
| 38 | + <Route path="/tasks" element={<TasksView />} /> | |
| 39 | + <Route path="/approvals" element={<ApprovalsView />} /> | |
| 40 | + <Route path="/tools" element={<ToolsRoute />} /> | |
| 41 | + <Route path="/organization" element={<OrganizationSettingsView />} /> | |
| 42 | + <Route path="/onboarding" element={<OnboardingView />} /> | |
| 43 | + <Route path="/members" element={<Navigate to="/organization?tab=members" replace />} /> | |
| 44 | + <Route path="/billing" element={<Navigate to="/organization?tab=billing" replace />} /> | |
| 45 | + <Route path="*" element={<Navigate to="/" replace />} /> | |
| 46 | + </Route> | |
| 47 | + </Routes> | |
| 48 | + </BrowserRouter> | |
| 49 | + ); | |
| 50 | +} | |
| @@ -10,7 +10,7 @@ import { · 9 unmodified lines | ||
| 10 | 10 | } from "react"; |
| 11 | 11 | import { useMutation, useQuery as useConvexQuery } from "convex/react"; |
| 12 | 12 | import { useQuery as useTanstackQuery } from "@tanstack/react-query"; |
| 13 | −import { workosEnabled } from "@/lib/auth-capabilities"; | |
| 13 | +import { anonymousDemoEnabled, workosEnabled } from "@/lib/auth-capabilities"; | |
| 14 | 14 | import { useWorkosAuthLoading } from "@/lib/convex-provider"; |
| 15 | 15 | import { convexApi } from "@/lib/convex-api"; |
| 16 | 16 | import type { AnonymousContext } from "./types"; |
| @@ -54,6 +54,8 @@ interface SessionState { · 37 unmodified lines | ||
| 54 | 54 | iconFile?: File | null, |
| 55 | 55 | organizationId?: Id<"organizations">, |
| 56 | 56 | ) => Promise<void>; |
| 57 | + creatingAnonymousWorkspace: boolean; | |
| 58 | + createAnonymousWorkspace: () => Promise<void>; | |
| 57 | 59 | isSignedInToWorkos: boolean; |
| 58 | 60 | workosProfile: { |
| 59 | 61 | name: string; |
| @@ -95,6 +97,8 @@ const SessionContext = createContext<SessionState>({ · 35 unmodified lines | ||
| 95 | 97 | switchWorkspace: () => {}, |
| 96 | 98 | creatingWorkspace: false, |
| 97 | 99 | createWorkspace: async () => {}, |
| 100 | + creatingAnonymousWorkspace: false, | |
| 101 | + createAnonymousWorkspace: async () => {}, | |
| 98 | 102 | isSignedInToWorkos: false, |
| 99 | 103 | workosProfile: null, |
| 100 | 104 | resetWorkspace: async () => {}, |
| @@ -143,13 +147,20 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 42 unmodified lines | ||
| 143 | 147 | const createWorkspaceMutation = useMutation(convexApi.workspaces.create); |
| 144 | 148 | const generateWorkspaceIconUploadUrl = useMutation(convexApi.workspaces.generateWorkspaceIconUploadUrl); |
| 145 | 149 | const [creatingWorkspace, setCreatingWorkspace] = useState(false); |
| 150 | + const [creatingAnonymousWorkspace, setCreatingAnonymousWorkspace] = useState(false); | |
| 151 | + const [manualGuestContext, setManualGuestContext] = useState<AnonymousContext | null>(null); | |
| 146 | 152 | |
| 147 | 153 | const bootstrapSessionQuery = useTanstackQuery({ |
| 148 | 154 | queryKey: ["session-bootstrap", storedSessionId ?? "new"], |
| 155 | + enabled: storedSessionId !== null, | |
| 149 | 156 | retry: false, |
| 150 | 157 | staleTime: Number.POSITIVE_INFINITY, |
| 151 | 158 | queryFn: async () => { |
| 152 | − const context = await bootstrapAnonymousSession({ sessionId: storedSessionId ?? undefined }); | |
| 159 | + if (!storedSessionId) { | |
| 160 | + throw new Error("No guest session id available"); | |
| 161 | + } | |
| 162 | + | |
| 163 | + const context = await bootstrapAnonymousSession({ sessionId: storedSessionId }); | |
| 153 | 164 | localStorage.setItem(SESSION_KEY, context.sessionId); |
| 154 | 165 | if (context.sessionId !== storedSessionId) { |
| 155 | 166 | setStoredSessionId(context.sessionId); |
| @@ -158,7 +169,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 2 unmodified lines | ||
| 158 | 169 | }, |
| 159 | 170 | }); |
| 160 | 171 | |
| 161 | − const guestContext: AnonymousContext | null = bootstrapSessionQuery.data ?? null; | |
| 172 | + const guestContext: AnonymousContext | null = manualGuestContext ?? bootstrapSessionQuery.data ?? null; | |
| 162 | 173 | |
| 163 | 174 | const account = useConvexQuery( |
| 164 | 175 | convexApi.app.getCurrentAccount, |
| @@ -204,9 +215,33 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 39 unmodified lines | ||
| 204 | 215 | localStorage.removeItem(ACTIVE_WORKSPACE_KEY); |
| 205 | 216 | setStoredSessionId(null); |
| 206 | 217 | setActiveWorkspaceId(null); |
| 218 | + setManualGuestContext(null); | |
| 207 | 219 | setRuntimeError(null); |
| 208 | 220 | }, []); |
| 209 | 221 | |
| 222 | + const createAnonymousWorkspace = useCallback(async () => { | |
| 223 | + if (!anonymousDemoEnabled) { | |
| 224 | + throw new Error("Anonymous workspace creation is disabled"); | |
| 225 | + } | |
| 226 | + | |
| 227 | + setRuntimeError(null); | |
| 228 | + setCreatingAnonymousWorkspace(true); | |
| 229 | + try { | |
| 230 | + const context = await bootstrapAnonymousSession({}); | |
| 231 | + localStorage.setItem(SESSION_KEY, context.sessionId); | |
| 232 | + localStorage.setItem(ACTIVE_WORKSPACE_KEY, context.workspaceId); | |
| 233 | + setStoredSessionId(context.sessionId); | |
| 234 | + setActiveWorkspaceId(context.workspaceId as Id<"workspaces">); | |
| 235 | + setManualGuestContext(context); | |
| 236 | + } catch (cause) { | |
| 237 | + const message = cause instanceof Error ? cause.message : "Failed to create anonymous workspace"; | |
| 238 | + setRuntimeError(message); | |
| 239 | + throw cause; | |
| 240 | + } finally { | |
| 241 | + setCreatingAnonymousWorkspace(false); | |
| 242 | + } | |
| 243 | + }, [bootstrapAnonymousSession]); | |
| 244 | + | |
| 210 | 245 | const switchWorkspace = useCallback((workspaceId: Id<"workspaces">) => { |
| 211 | 246 | setActiveWorkspaceId(workspaceId); |
| 212 | 247 | localStorage.setItem(ACTIVE_WORKSPACE_KEY, workspaceId); |
| @@ -318,9 +353,9 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 105 unmodified lines | ||
| 318 | 353 | const context = workosContext ?? ((workosStillLoading || shouldBlockGuestFallback) ? null : guestContext); |
| 319 | 354 | |
| 320 | 355 | const bootstrapSessionError = |
| 321 | − bootstrapSessionQuery.error instanceof Error | |
| 356 | + storedSessionId && bootstrapSessionQuery.error instanceof Error | |
| 322 | 357 | ? bootstrapSessionQuery.error.message |
| 323 | − : bootstrapSessionQuery.error | |
| 358 | + : storedSessionId && bootstrapSessionQuery.error | |
| 324 | 359 | ? "Failed to bootstrap session" |
| 325 | 360 | : null; |
| 326 | 361 | const bootstrapWorkosError = |
| @@ -332,7 +367,8 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 5 unmodified lines | ||
| 332 | 367 | const error = runtimeError ?? bootstrapSessionError ?? bootstrapWorkosError; |
| 333 | 368 | |
| 334 | 369 | const effectiveLoading = !context && !error && ( |
| 335 | − bootstrapSessionQuery.isLoading | |
| 370 | + creatingAnonymousWorkspace | |
| 371 | + || (storedSessionId !== null && bootstrapSessionQuery.isLoading) | |
| 336 | 372 | || workosStillLoading |
| 337 | 373 | || bootstrapWorkosAccountQuery.isFetching |
| 338 | 374 | ); |
| @@ -379,6 +415,8 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 40 unmodified lines | ||
| 379 | 415 | switchWorkspace, |
| 380 | 416 | creatingWorkspace, |
| 381 | 417 | createWorkspace, |
| 418 | + creatingAnonymousWorkspace, | |
| 419 | + createAnonymousWorkspace, | |
| 382 | 420 | isSignedInToWorkos: Boolean(account && account.provider === "workos"), |
| 383 | 421 | workosProfile: |
| 384 | 422 | account && account.provider === "workos" |