- 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 | "radix-ui": "^1.4.3", | 106 | "radix-ui": "^1.4.3", |
| 107 | "react": "19.2.3", | 107 | "react": "19.2.3", |
| 108 | "react-dom": "19.2.3", | 108 | "react-dom": "19.2.3", |
| 109 | "react-router": "^7.9.5", | ||
| 109 | "shiki": "^3.22.0", | 110 | "shiki": "^3.22.0", |
| 110 | "sonner": "^2.0.7", | 111 | "sonner": "^2.0.7", |
| 111 | "streamdown": "^2.1.0", | 112 | "streamdown": "^2.1.0", |
| @@ -317,7 +318,7 @@ · 205 unmodified lines | |||
| 317 | 318 | ||
| 318 | "@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 | "@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 | "@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 | "@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 | "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 | "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 | "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=="], | 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 | "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], | 2197 | "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], |
| @@ -2269,6 +2272,8 @@ · 74 unmodified lines | |||
| 2269 | 2272 | ||
| 2270 | "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=="], | 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 | "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=="], | 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 | "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=="], | 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 | import type { NextConfig } from "next"; | 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 | const nextConfig: NextConfig = { | 15 | const nextConfig: NextConfig = { |
| 3 | transpilePackages: ["@executor/contracts"], | 16 | transpilePackages: ["@executor/contracts"], |
| 4 | env: { | 17 | env: { |
| @@ -8,6 +21,14 @@ const nextConfig: NextConfig = { · 3 unmodified lines | |||
| 8 | NEXT_PUBLIC_WORKOS_CLIENT_ID: process.env.WORKOS_CLIENT_ID, | 21 | NEXT_PUBLIC_WORKOS_CLIENT_ID: process.env.WORKOS_CLIENT_ID, |
| 9 | NEXT_PUBLIC_STRIPE_PRICE_ID: process.env.STRIPE_PRICE_ID, | 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 | export default nextConfig; | 34 | export default nextConfig; |
| @@ -28,6 +28,7 @@ · 27 unmodified lines | |||
| 28 | "radix-ui": "^1.4.3", | 28 | "radix-ui": "^1.4.3", |
| 29 | "react": "19.2.3", | 29 | "react": "19.2.3", |
| 30 | "react-dom": "19.2.3", | 30 | "react-dom": "19.2.3", |
| 31 | "react-router": "^7.9.5", | ||
| 31 | "shiki": "^3.22.0", | 32 | "shiki": "^3.22.0", |
| 32 | "sonner": "^2.0.7", | 33 | "sonner": "^2.0.7", |
| 33 | "streamdown": "^2.1.0", | 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 | export default function ApprovalsPage() { | 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 | import { JetBrains_Mono, Inter } from "next/font/google"; | 2 | import { JetBrains_Mono, Inter } from "next/font/google"; |
| 3 | import { ThemeProvider } from "next-themes"; | 3 | import { ThemeProvider } from "next-themes"; |
| 4 | import { Toaster } from "@/components/ui/sonner"; | 4 | import { Toaster } from "@/components/ui/sonner"; |
| 5 | import { AppShell } from "@/components/app-shell"; | ||
| 6 | import { AppConvexProvider } from "@/lib/convex-provider"; | 5 | import { AppConvexProvider } from "@/lib/convex-provider"; |
| 7 | import { QueryProvider } from "@/lib/query-provider"; | 6 | import { QueryProvider } from "@/lib/query-provider"; |
| 8 | import { SessionProvider } from "@/lib/session-context"; | 7 | import { SessionProvider } from "@/lib/session-context"; |
| @@ -42,7 +41,7 @@ export default function RootLayout({ · 33 unmodified lines | |||
| 42 | <AppConvexProvider> | 41 | <AppConvexProvider> |
| 43 | <QueryProvider> | 42 | <QueryProvider> |
| 44 | <SessionProvider> | 43 | <SessionProvider> |
| 45 | <AppShell>{children}</AppShell> | 44 | {children} |
| 46 | </SessionProvider> | 45 | </SessionProvider> |
| 47 | </QueryProvider> | 46 | </QueryProvider> |
| 48 | </AppConvexProvider> | 47 | </AppConvexProvider> |
| @@ -1,137 +1,5 @@ | |||
| 1 | "use client"; | 1 | import { StaticAppShell } from "@/components/static-app-shell"; |
| 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"; | ||
| 20 | 2 | ||
| 21 | export default function OnboardingPage() { | 3 | export default function OnboardingPage() { |
| 22 | const router = useRouter(); | 4 | return <StaticAppShell />; |
| 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 | ); | ||
| 137 | } | 5 | } |
| @@ -1,10 +1,5 @@ | |||
| 1 | import { Suspense } from "react"; | 1 | import { StaticAppShell } from "@/components/static-app-shell"; |
| 2 | import { OrganizationSettingsView } from "@/components/organization-settings-view"; | ||
| 3 | 2 | ||
| 4 | export default function OrganizationPage() { | 3 | export default function OrganizationPage() { |
| 5 | return ( | 4 | return <StaticAppShell />; |
| 6 | <Suspense> | ||
| 7 | <OrganizationSettingsView /> | ||
| 8 | </Suspense> | ||
| 9 | ); | ||
| 10 | } | 5 | } |
| @@ -1,5 +1,5 @@ | |||
| 1 | import { DashboardView } from "@/components/dashboard-view"; | 1 | import { StaticAppShell } from "@/components/static-app-shell"; |
| 2 | 2 | ||
| 3 | export default function DashboardPage() { | 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"; | 1 | import { StaticAppShell } from "@/components/static-app-shell"; |
| 2 | import { TasksView } from "@/components/tasks-view"; | ||
| 3 | 2 | ||
| 4 | export default function TasksPage() { | 3 | export default function TasksPage() { |
| 5 | return ( | 4 | return <StaticAppShell />; |
| 6 | <Suspense> | ||
| 7 | <TasksView /> | ||
| 8 | </Suspense> | ||
| 9 | ); | ||
| 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({ | 3 | export default function ToolsPage() { |
| 4 | searchParams, | 4 | return <StaticAppShell />; |
| 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 | ); | ||
| 16 | } | 5 | } |
| @@ -1,8 +1,7 @@ | |||
| 1 | "use client"; | 1 | "use client"; |
| 2 | 2 | ||
| 3 | import { Suspense, useEffect, useMemo, useState, type FormEvent } from "react"; | 3 | import { Suspense, useEffect, useMemo, useState, type FormEvent } from "react"; |
| 4 | import Link from "next/link"; | 4 | import { Link, useLocation, useNavigate, useSearchParams } from "react-router"; |
| 5 | import { usePathname, useRouter, useSearchParams } from "next/navigation"; | ||
| 6 | import { useMutation } from "convex/react"; | 5 | import { useMutation } from "convex/react"; |
| 7 | import { | 6 | import { |
| 8 | LayoutDashboard, | 7 | LayoutDashboard, |
| @@ -38,7 +37,7 @@ import { · 29 unmodified lines | |||
| 38 | import { Input } from "@/components/ui/input"; | 37 | import { Input } from "@/components/ui/input"; |
| 39 | import { useSession } from "@/lib/session-context"; | 38 | import { useSession } from "@/lib/session-context"; |
| 40 | import { convexApi } from "@/lib/convex-api"; | 39 | import { convexApi } from "@/lib/convex-api"; |
| 41 | import { workosEnabled } from "@/lib/auth-capabilities"; | 40 | import { anonymousDemoEnabled, workosEnabled } from "@/lib/auth-capabilities"; |
| 42 | import { ThemeSwitcher } from "@/components/theme-switcher"; | 41 | import { ThemeSwitcher } from "@/components/theme-switcher"; |
| 43 | import { ApprovalNotifier } from "@/components/approval-notifier"; | 42 | import { ApprovalNotifier } from "@/components/approval-notifier"; |
| 44 | import { cn } from "@/lib/utils"; | 43 | import { cn } from "@/lib/utils"; |
| @@ -51,7 +50,8 @@ const NAV_ITEMS = [ · 6 unmodified lines | |||
| 51 | ]; | 50 | ]; |
| 52 | 51 | ||
| 53 | function NavLinks({ onClick }: { onClick?: () => void }) { | 52 | function NavLinks({ onClick }: { onClick?: () => void }) { |
| 54 | const pathname = usePathname(); | 53 | const location = useLocation(); |
| 54 | const pathname = location.pathname; | ||
| 55 | 55 | ||
| 56 | return ( | 56 | return ( |
| 57 | <nav className="flex flex-col gap-1"> | 57 | <nav className="flex flex-col gap-1"> |
| @@ -63,7 +63,7 @@ function NavLinks({ onClick }: { onClick?: () => void }) { · 5 unmodified lines | |||
| 63 | return ( | 63 | return ( |
| 64 | <Link | 64 | <Link |
| 65 | key={item.href} | 65 | key={item.href} |
| 66 | href={item.href} | 66 | to={item.href} |
| 67 | onClick={onClick} | 67 | onClick={onClick} |
| 68 | className={cn( | 68 | className={cn( |
| 69 | "flex items-center gap-3 px-3 py-2.5 rounded-md text-sm font-medium transition-colors", | 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 | function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) { | 84 | function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) { |
| 85 | const router = useRouter(); | 85 | const navigate = useNavigate(); |
| 86 | const createOrganizationMutation = useMutation(convexApi.organizations.create); | 86 | const createOrganizationMutation = useMutation(convexApi.organizations.create); |
| 87 | const { | 87 | const { |
| 88 | context, | 88 | context, |
| 89 | mode, | 89 | mode, |
| 90 | clientConfig, | ||
| 91 | workspaces, | 90 | workspaces, |
| 92 | switchWorkspace, | 91 | switchWorkspace, |
| 93 | creatingWorkspace, | 92 | creatingWorkspace, |
| @@ -261,7 +260,7 @@ function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) { · 167 unmodified lines | |||
| 261 | 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" | 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 | onClick={() => { | 261 | onClick={() => { |
| 263 | switchWorkspace(settingsWorkspace.id); | 262 | switchWorkspace(settingsWorkspace.id); |
| 264 | router.push("/organization?tab=members"); | 263 | navigate("/organization?tab=members"); |
| 265 | }} | 264 | }} |
| 266 | aria-label={`Open ${group.organizationName} settings`} | 265 | aria-label={`Open ${group.organizationName} settings`} |
| 267 | title="Organization settings" | 266 | title="Organization settings" |
| @@ -402,14 +401,13 @@ function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) { · 134 unmodified lines | |||
| 402 | function SessionInfo() { | 401 | function SessionInfo() { |
| 403 | const { | 402 | const { |
| 404 | loading, | 403 | loading, |
| 405 | clientConfig, | ||
| 406 | isSignedInToWorkos, | 404 | isSignedInToWorkos, |
| 407 | workosProfile, | 405 | workosProfile, |
| 408 | context, | 406 | context, |
| 409 | workspaces, | 407 | workspaces, |
| 410 | resetWorkspace, | 408 | resetWorkspace, |
| 411 | } = useSession(); | 409 | } = useSession(); |
| 412 | const searchParams = useSearchParams(); | 410 | const [searchParams] = useSearchParams(); |
| 413 | const deleteCurrentAccountMutation = useMutation(convexApi.accounts.deleteCurrentAccount); | 411 | const deleteCurrentAccountMutation = useMutation(convexApi.accounts.deleteCurrentAccount); |
| 414 | const [accountSettingsOpen, setAccountSettingsOpen] = useState(false); | 412 | const [accountSettingsOpen, setAccountSettingsOpen] = useState(false); |
| 415 | const [deleteConfirmText, setDeleteConfirmText] = useState(""); | 413 | const [deleteConfirmText, setDeleteConfirmText] = useState(""); |
| @@ -523,7 +521,7 @@ function SessionInfo() { · 107 unmodified lines | |||
| 523 | Account Settings | 521 | Account Settings |
| 524 | </DropdownMenuItem> | 522 | </DropdownMenuItem> |
| 525 | <DropdownMenuItem asChild className="text-xs"> | 523 | <DropdownMenuItem asChild className="text-xs"> |
| 526 | <Link href="/sign-out">Sign out</Link> | 524 | <Link to="/sign-out" reloadDocument>Sign out</Link> |
| 527 | </DropdownMenuItem> | 525 | </DropdownMenuItem> |
| 528 | </DropdownMenuContent> | 526 | </DropdownMenuContent> |
| 529 | </DropdownMenu> | 527 | </DropdownMenu> |
| @@ -587,7 +585,7 @@ function SessionInfo() { · 57 unmodified lines | |||
| 587 | ) : ( | 585 | ) : ( |
| 588 | <div className="px-3 py-3 space-y-2"> | 586 | <div className="px-3 py-3 space-y-2"> |
| 589 | {workosEnabled ? ( | 587 | {workosEnabled ? ( |
| 590 | <Link href={signInHref} className="block"> | 588 | <Link to={signInHref} reloadDocument className="block"> |
| 591 | <Button | 589 | <Button |
| 592 | variant="outline" | 590 | variant="outline" |
| 593 | className="w-full h-9 justify-center gap-2 text-xs font-medium" | 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 | export function AppShell({ children }: { children: React.ReactNode }) { | 733 | export function AppShell({ children }: { children: React.ReactNode }) { |
| 667 | const pathname = usePathname(); | 734 | const location = useLocation(); |
| 668 | const router = useRouter(); | 735 | const pathname = location.pathname; |
| 736 | const navigate = useNavigate(); | ||
| 669 | const { loading, organizations, organizationsLoading, isSignedInToWorkos } = useSession(); | 737 | const { loading, organizations, organizationsLoading, isSignedInToWorkos } = useSession(); |
| 670 | 738 | ||
| 671 | const onOnboardingRoute = pathname.startsWith("/onboarding"); | 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 | if (needsOnboarding && !onOnboardingRoute) { | 747 | if (needsOnboarding && !onOnboardingRoute) { |
| 680 | router.replace("/onboarding"); | 748 | navigate("/onboarding", { replace: true }); |
| 681 | return; | 749 | return; |
| 682 | } | 750 | } |
| 683 | 751 | ||
| 684 | if (!needsOnboarding && onOnboardingRoute && organizations.length > 0) { | 752 | if (!needsOnboarding && onOnboardingRoute && organizations.length > 0) { |
| 685 | router.replace("/"); | 753 | navigate("/", { replace: true }); |
| 686 | } | 754 | } |
| 687 | }, [ | 755 | }, [ |
| 688 | loading, | 756 | loading, |
| @@ -690,7 +758,7 @@ export function AppShell({ children }: { children: React.ReactNode }) { · 1 unmodified line | |||
| 690 | needsOnboarding, | 758 | needsOnboarding, |
| 691 | onOnboardingRoute, | 759 | onOnboardingRoute, |
| 692 | organizations.length, | 760 | organizations.length, |
| 693 | router, | 761 | navigate, |
| 694 | ]); | 762 | ]); |
| 695 | 763 | ||
| 696 | if (isSignedInToWorkos && organizationsLoading && !onOnboardingRoute) { | 764 | if (isSignedInToWorkos && organizationsLoading && !onOnboardingRoute) { |
| @@ -712,6 +780,7 @@ export function AppShell({ children }: { children: React.ReactNode }) { · 15 unmodified lines | |||
| 712 | return ( | 780 | return ( |
| 713 | <div className="flex min-h-screen"> | 781 | <div className="flex min-h-screen"> |
| 714 | <ApprovalNotifier /> | 782 | <ApprovalNotifier /> |
| 783 | <NoOrganizationModal enabled /> | ||
| 715 | <Sidebar /> | 784 | <Sidebar /> |
| 716 | <div className="flex-1 flex flex-col min-w-0"> | 785 | <div className="flex-1 flex flex-col min-w-0"> |
| 717 | <MobileHeader /> | 786 | <MobileHeader /> |
| @@ -1,7 +1,7 @@ | |||
| 1 | "use client"; | 1 | "use client"; |
| 2 | 2 | ||
| 3 | import { useEffect, useRef } from "react"; | 3 | import { useEffect, useRef } from "react"; |
| 4 | import { useRouter } from "next/navigation"; | 4 | import { useNavigate } from "react-router"; |
| 5 | import { useQuery } from "convex/react"; | 5 | import { useQuery } from "convex/react"; |
| 6 | import { toast } from "sonner"; | 6 | import { toast } from "sonner"; |
| 7 | import { useSession } from "@/lib/session-context"; | 7 | import { useSession } from "@/lib/session-context"; |
| @@ -9,7 +9,7 @@ import { convexApi } from "@/lib/convex-api"; · 1 unmodified line | |||
| 9 | import type { PendingApprovalRecord } from "@/lib/types"; | 9 | import type { PendingApprovalRecord } from "@/lib/types"; |
| 10 | 10 | ||
| 11 | export function ApprovalNotifier() { | 11 | export function ApprovalNotifier() { |
| 12 | const router = useRouter(); | 12 | const navigate = useNavigate(); |
| 13 | const { context } = useSession(); | 13 | const { context } = useSession(); |
| 14 | const seenApprovalIdsRef = useRef<Set<string>>(new Set()); | 14 | const seenApprovalIdsRef = useRef<Set<string>>(new Set()); |
| 15 | const activeWorkspaceRef = useRef<string | null>(null); | 15 | const activeWorkspaceRef = useRef<string | null>(null); |
| @@ -58,12 +58,12 @@ export function ApprovalNotifier() { · 42 unmodified lines | |||
| 58 | : "Open approvals to review pending tool calls.", | 58 | : "Open approvals to review pending tool calls.", |
| 59 | action: { | 59 | action: { |
| 60 | label: "Review", | 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 | return null; | 68 | return null; |
| 69 | } | 69 | } |
| @@ -1,7 +1,7 @@ | |||
| 1 | "use client"; | 1 | "use client"; |
| 2 | 2 | ||
| 3 | import { useMemo, useState } from "react"; | 3 | import { useMemo, useState } from "react"; |
| 4 | import { useSearchParams } from "next/navigation"; | 4 | import { useSearchParams } from "react-router"; |
| 5 | import { useAction, useMutation, useQuery } from "convex/react"; | 5 | import { useAction, useMutation, useQuery } from "convex/react"; |
| 6 | import { AlertTriangle, BadgeCheck, CreditCard, RefreshCcw } from "lucide-react"; | 6 | import { AlertTriangle, BadgeCheck, CreditCard, RefreshCcw } from "lucide-react"; |
| 7 | import { PageHeader } from "@/components/page-header"; | 7 | import { PageHeader } from "@/components/page-header"; |
| @@ -50,7 +50,7 @@ function formatTimestamp(value: number | null | undefined): string { · 42 unmodified lines | |||
| 50 | 50 | ||
| 51 | export function BillingView({ showHeader = true }: BillingViewProps) { | 51 | export function BillingView({ showHeader = true }: BillingViewProps) { |
| 52 | const { context, organizations, organizationsLoading, workspaces } = useSession(); | 52 | const { context, organizations, organizationsLoading, workspaces } = useSession(); |
| 53 | const searchParams = useSearchParams(); | 53 | const [searchParams] = useSearchParams(); |
| 54 | const [actionState, setActionState] = useState<"idle" | "running" | "success" | "error">("idle"); | 54 | const [actionState, setActionState] = useState<"idle" | "running" | "success" | "error">("idle"); |
| 55 | const [actionMessage, setActionMessage] = useState<string | null>(null); | 55 | const [actionMessage, setActionMessage] = useState<string | null>(null); |
| 56 | const [priceId, setPriceId] = useState(() => process.env.NEXT_PUBLIC_STRIPE_PRICE_ID ?? ""); | 56 | const [priceId, setPriceId] = useState(() => process.env.NEXT_PUBLIC_STRIPE_PRICE_ID ?? ""); |
| @@ -1,7 +1,7 @@ | |||
| 1 | "use client"; | 1 | "use client"; |
| 2 | 2 | ||
| 3 | import { useMemo } from "react"; | 3 | import { useMemo } from "react"; |
| 4 | import { useRouter } from "next/navigation"; | 4 | import { useNavigate } from "react-router"; |
| 5 | import { | 5 | import { |
| 6 | Play, | 6 | Play, |
| 7 | ShieldCheck, | 7 | ShieldCheck, |
| @@ -67,10 +67,10 @@ function StatCard({ · 59 unmodified lines | |||
| 67 | } | 67 | } |
| 68 | 68 | ||
| 69 | function PendingApprovalRow({ approval }: { approval: PendingApprovalRecord }) { | 69 | function PendingApprovalRow({ approval }: { approval: PendingApprovalRecord }) { |
| 70 | const router = useRouter(); | 70 | const navigate = useNavigate(); |
| 71 | return ( | 71 | return ( |
| 72 | <button | 72 | <button |
| 73 | onClick={() => router.push("/approvals")} | 73 | onClick={() => navigate("/approvals")} |
| 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" | 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 | <div className="h-2 w-2 rounded-full bg-terminal-amber pulse-dot shrink-0" /> | 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 | function RecentTaskRow({ task }: { task: TaskRecord }) { | 90 | function RecentTaskRow({ task }: { task: TaskRecord }) { |
| 91 | const router = useRouter(); | 91 | const navigate = useNavigate(); |
| 92 | return ( | 92 | return ( |
| 93 | <button | 93 | <button |
| 94 | onClick={() => router.push(`/tasks?selected=${task.id}`)} | 94 | onClick={() => navigate(`/tasks?selected=${task.id}`)} |
| 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" | 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 | <div className="flex-1 min-w-0"> | 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 | function ToolsSummaryCard({ tools }: { tools: ToolDescriptor[] }) { | 124 | function ToolsSummaryCard({ tools }: { tools: ToolDescriptor[] }) { |
| 125 | const router = useRouter(); | 125 | const navigate = useNavigate(); |
| 126 | 126 | ||
| 127 | const groups = useMemo(() => { | 127 | const groups = useMemo(() => { |
| 128 | const map = new Map< | 128 | const map = new Map< |
| @@ -180,7 +180,7 @@ function ToolsSummaryCard({ tools }: { tools: ToolDescriptor[] }) { · 51 unmodified lines | |||
| 180 | variant="ghost" | 180 | variant="ghost" |
| 181 | size="sm" | 181 | size="sm" |
| 182 | className="text-xs h-7" | 182 | className="text-xs h-7" |
| 183 | onClick={() => router.push("/tools")} | 183 | onClick={() => navigate("/tools")} |
| 184 | > | 184 | > |
| 185 | Manage | 185 | Manage |
| 186 | <ArrowRight className="h-3 w-3 ml-1" /> | 186 | <ArrowRight className="h-3 w-3 ml-1" /> |
| @@ -194,7 +194,7 @@ function ToolsSummaryCard({ tools }: { tools: ToolDescriptor[] }) { · 7 unmodified lines | |||
| 194 | return ( | 194 | return ( |
| 195 | <button | 195 | <button |
| 196 | key={group.name} | 196 | key={group.name} |
| 197 | onClick={() => router.push(`/tools?source=${encodeURIComponent(group.name)}`)} | 197 | onClick={() => navigate(`/tools?source=${encodeURIComponent(group.name)}`)} |
| 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" | 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 | <div className="h-7 w-7 rounded bg-muted flex items-center justify-center shrink-0"> | 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 | "use client"; | 1 | "use client"; |
| 2 | 2 | ||
| 3 | import { useMemo } from "react"; | 3 | import { useMemo } from "react"; |
| 4 | import { usePathname, useRouter, useSearchParams } from "next/navigation"; | 4 | import { useLocation, useNavigate, useSearchParams } from "react-router"; |
| 5 | import { CreditCard, Users } from "lucide-react"; | 5 | import { CreditCard, Users } from "lucide-react"; |
| 6 | import { BillingView } from "@/components/billing-view"; | 6 | import { BillingView } from "@/components/billing-view"; |
| 7 | import { MembersView } from "@/components/members-view"; | 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 | export function OrganizationSettingsView() { | 17 | export function OrganizationSettingsView() { |
| 18 | const router = useRouter(); | 18 | const navigate = useNavigate(); |
| 19 | const pathname = usePathname(); | 19 | const location = useLocation(); |
| 20 | const searchParams = useSearchParams(); | 20 | const pathname = location.pathname; |
| 21 | const [searchParams] = useSearchParams(); | ||
| 21 | 22 | ||
| 22 | const tab = useMemo(() => normalizeTab(searchParams.get("tab")), [searchParams]); | 23 | const tab = useMemo(() => normalizeTab(searchParams.get("tab")), [searchParams]); |
| 23 | 24 | ||
| @@ -25,7 +26,7 @@ export function OrganizationSettingsView() { · 1 unmodified line | |||
| 25 | const normalizedTab = normalizeTab(nextTab); | 26 | const normalizedTab = normalizeTab(nextTab); |
| 26 | const nextSearch = new URLSearchParams(searchParams.toString()); | 27 | const nextSearch = new URLSearchParams(searchParams.toString()); |
| 27 | nextSearch.set("tab", normalizedTab); | 28 | nextSearch.set("tab", normalizedTab); |
| 28 | router.replace(`${pathname}?${nextSearch.toString()}`); | 29 | navigate(`${pathname}?${nextSearch.toString()}`, { replace: true }); |
| 29 | }; | 30 | }; |
| 30 | 31 | ||
| 31 | return ( | 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 | "use client"; | 1 | "use client"; |
| 2 | 2 | ||
| 3 | import { useState, useCallback, useMemo } from "react"; | 3 | import { useState, useCallback, useMemo } from "react"; |
| 4 | import { useSearchParams, useRouter } from "next/navigation"; | 4 | import { useNavigate, useSearchParams } from "react-router"; |
| 5 | import { | 5 | import { |
| 6 | Play, | 6 | Play, |
| 7 | ChevronRight, | 7 | ChevronRight, |
| @@ -361,8 +361,8 @@ function TaskDetail({ · 353 unmodified lines | |||
| 361 | 361 | ||
| 362 | export function TasksView() { | 362 | export function TasksView() { |
| 363 | const { context, loading: sessionLoading } = useSession(); | 363 | const { context, loading: sessionLoading } = useSession(); |
| 364 | const router = useRouter(); | 364 | const navigate = useNavigate(); |
| 365 | const searchParams = useSearchParams(); | 365 | const [searchParams] = useSearchParams(); |
| 366 | const selectedId = searchParams.get("selected"); | 366 | const selectedId = searchParams.get("selected"); |
| 367 | 367 | ||
| 368 | const tasks = useQuery( | 368 | const tasks = useQuery( |
| @@ -377,12 +377,12 @@ export function TasksView() { · 8 unmodified lines | |||
| 377 | const selectTask = useCallback( | 377 | const selectTask = useCallback( |
| 378 | (taskId: string | null) => { | 378 | (taskId: string | null) => { |
| 379 | if (taskId) { | 379 | if (taskId) { |
| 380 | router.push(`/tasks?selected=${taskId}`, { scroll: false }); | 380 | navigate(`/tasks?selected=${taskId}`); |
| 381 | } else { | 381 | } else { |
| 382 | router.push("/tasks", { scroll: false }); | 382 | navigate("/tasks"); |
| 383 | } | 383 | } |
| 384 | }, | 384 | }, |
| 385 | [router], | 385 | [navigate], |
| 386 | ); | 386 | ); |
| 387 | 387 | ||
| 388 | if (sessionLoading) { | 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 | } from "react"; | 10 | } from "react"; |
| 11 | import { useMutation, useQuery as useConvexQuery } from "convex/react"; | 11 | import { useMutation, useQuery as useConvexQuery } from "convex/react"; |
| 12 | import { useQuery as useTanstackQuery } from "@tanstack/react-query"; | 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 | import { useWorkosAuthLoading } from "@/lib/convex-provider"; | 14 | import { useWorkosAuthLoading } from "@/lib/convex-provider"; |
| 15 | import { convexApi } from "@/lib/convex-api"; | 15 | import { convexApi } from "@/lib/convex-api"; |
| 16 | import type { AnonymousContext } from "./types"; | 16 | import type { AnonymousContext } from "./types"; |
| @@ -54,6 +54,8 @@ interface SessionState { · 37 unmodified lines | |||
| 54 | iconFile?: File | null, | 54 | iconFile?: File | null, |
| 55 | organizationId?: Id<"organizations">, | 55 | organizationId?: Id<"organizations">, |
| 56 | ) => Promise<void>; | 56 | ) => Promise<void>; |
| 57 | creatingAnonymousWorkspace: boolean; | ||
| 58 | createAnonymousWorkspace: () => Promise<void>; | ||
| 57 | isSignedInToWorkos: boolean; | 59 | isSignedInToWorkos: boolean; |
| 58 | workosProfile: { | 60 | workosProfile: { |
| 59 | name: string; | 61 | name: string; |
| @@ -95,6 +97,8 @@ const SessionContext = createContext<SessionState>({ · 35 unmodified lines | |||
| 95 | switchWorkspace: () => {}, | 97 | switchWorkspace: () => {}, |
| 96 | creatingWorkspace: false, | 98 | creatingWorkspace: false, |
| 97 | createWorkspace: async () => {}, | 99 | createWorkspace: async () => {}, |
| 100 | creatingAnonymousWorkspace: false, | ||
| 101 | createAnonymousWorkspace: async () => {}, | ||
| 98 | isSignedInToWorkos: false, | 102 | isSignedInToWorkos: false, |
| 99 | workosProfile: null, | 103 | workosProfile: null, |
| 100 | resetWorkspace: async () => {}, | 104 | resetWorkspace: async () => {}, |
| @@ -143,13 +147,20 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 42 unmodified lines | |||
| 143 | const createWorkspaceMutation = useMutation(convexApi.workspaces.create); | 147 | const createWorkspaceMutation = useMutation(convexApi.workspaces.create); |
| 144 | const generateWorkspaceIconUploadUrl = useMutation(convexApi.workspaces.generateWorkspaceIconUploadUrl); | 148 | const generateWorkspaceIconUploadUrl = useMutation(convexApi.workspaces.generateWorkspaceIconUploadUrl); |
| 145 | const [creatingWorkspace, setCreatingWorkspace] = useState(false); | 149 | const [creatingWorkspace, setCreatingWorkspace] = useState(false); |
| 150 | const [creatingAnonymousWorkspace, setCreatingAnonymousWorkspace] = useState(false); | ||
| 151 | const [manualGuestContext, setManualGuestContext] = useState<AnonymousContext | null>(null); | ||
| 146 | 152 | ||
| 147 | const bootstrapSessionQuery = useTanstackQuery({ | 153 | const bootstrapSessionQuery = useTanstackQuery({ |
| 148 | queryKey: ["session-bootstrap", storedSessionId ?? "new"], | 154 | queryKey: ["session-bootstrap", storedSessionId ?? "new"], |
| 155 | enabled: storedSessionId !== null, | ||
| 149 | retry: false, | 156 | retry: false, |
| 150 | staleTime: Number.POSITIVE_INFINITY, | 157 | staleTime: Number.POSITIVE_INFINITY, |
| 151 | queryFn: async () => { | 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 | localStorage.setItem(SESSION_KEY, context.sessionId); | 164 | localStorage.setItem(SESSION_KEY, context.sessionId); |
| 154 | if (context.sessionId !== storedSessionId) { | 165 | if (context.sessionId !== storedSessionId) { |
| 155 | setStoredSessionId(context.sessionId); | 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 | const account = useConvexQuery( | 174 | const account = useConvexQuery( |
| 164 | convexApi.app.getCurrentAccount, | 175 | convexApi.app.getCurrentAccount, |
| @@ -204,9 +215,33 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 39 unmodified lines | |||
| 204 | localStorage.removeItem(ACTIVE_WORKSPACE_KEY); | 215 | localStorage.removeItem(ACTIVE_WORKSPACE_KEY); |
| 205 | setStoredSessionId(null); | 216 | setStoredSessionId(null); |
| 206 | setActiveWorkspaceId(null); | 217 | setActiveWorkspaceId(null); |
| 218 | setManualGuestContext(null); | ||
| 207 | setRuntimeError(null); | 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 | const switchWorkspace = useCallback((workspaceId: Id<"workspaces">) => { | 245 | const switchWorkspace = useCallback((workspaceId: Id<"workspaces">) => { |
| 211 | setActiveWorkspaceId(workspaceId); | 246 | setActiveWorkspaceId(workspaceId); |
| 212 | localStorage.setItem(ACTIVE_WORKSPACE_KEY, workspaceId); | 247 | localStorage.setItem(ACTIVE_WORKSPACE_KEY, workspaceId); |
| @@ -318,9 +353,9 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 105 unmodified lines | |||
| 318 | const context = workosContext ?? ((workosStillLoading || shouldBlockGuestFallback) ? null : guestContext); | 353 | const context = workosContext ?? ((workosStillLoading || shouldBlockGuestFallback) ? null : guestContext); |
| 319 | 354 | ||
| 320 | const bootstrapSessionError = | 355 | const bootstrapSessionError = |
| 321 | bootstrapSessionQuery.error instanceof Error | 356 | storedSessionId && bootstrapSessionQuery.error instanceof Error |
| 322 | ? bootstrapSessionQuery.error.message | 357 | ? bootstrapSessionQuery.error.message |
| 323 | : bootstrapSessionQuery.error | 358 | : storedSessionId && bootstrapSessionQuery.error |
| 324 | ? "Failed to bootstrap session" | 359 | ? "Failed to bootstrap session" |
| 325 | : null; | 360 | : null; |
| 326 | const bootstrapWorkosError = | 361 | const bootstrapWorkosError = |
| @@ -332,7 +367,8 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 5 unmodified lines | |||
| 332 | const error = runtimeError ?? bootstrapSessionError ?? bootstrapWorkosError; | 367 | const error = runtimeError ?? bootstrapSessionError ?? bootstrapWorkosError; |
| 333 | 368 | ||
| 334 | const effectiveLoading = !context && !error && ( | 369 | const effectiveLoading = !context && !error && ( |
| 335 | bootstrapSessionQuery.isLoading | 370 | creatingAnonymousWorkspace |
| 371 | || (storedSessionId !== null && bootstrapSessionQuery.isLoading) | ||
| 336 | || workosStillLoading | 372 | || workosStillLoading |
| 337 | || bootstrapWorkosAccountQuery.isFetching | 373 | || bootstrapWorkosAccountQuery.isFetching |
| 338 | ); | 374 | ); |
| @@ -379,6 +415,8 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 40 unmodified lines | |||
| 379 | switchWorkspace, | 415 | switchWorkspace, |
| 380 | creatingWorkspace, | 416 | creatingWorkspace, |
| 381 | createWorkspace, | 417 | createWorkspace, |
| 418 | creatingAnonymousWorkspace, | ||
| 419 | createAnonymousWorkspace, | ||
| 382 | isSignedInToWorkos: Boolean(account && account.provider === "workos"), | 420 | isSignedInToWorkos: Boolean(account && account.provider === "workos"), |
| 383 | workosProfile: | 421 | workosProfile: |
| 384 | account && account.provider === "workos" | 422 | account && account.provider === "workos" |