Skip to content

Commit 95e4a47

Browse files
committed
chore: prepare for opendocks migration
1 parent e483ae4 commit 95e4a47

10 files changed

Lines changed: 329 additions & 70 deletions

File tree

apps/web/.env.lcoal

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
VITE_CONVEX_URL=https://warmhearted-ferret-15.convex.cloud
2+
CONVEX_DEPLOYMENT=https://warmhearted-ferret-15.convex.cloud
3+
VITE_CLERK_PUBLISHABLE_KEY=pk_test_Y2FwaXRhbC1tZWVya2F0LTY2LmNsZXJrLmFjY291bnRzLmRldiQ
4+
CLERK_SECRET_KEY=sk_test_4LATHcVKJxFWr2ENhtlHWnxCNH0clN3KCPop7pJXeu
5+
CLERK_WEBHOOK_SECRET=sk_test_4LATHcVKJxFWr2ENhtlHWnxCNH0clN3KCPop7pJXeu
6+
ENCRYPTION_MASTER_KEY=<64-char-hex-from-generator>
7+
VITE_APP_URL=http://localhost:3000
8+
NODE_ENV=development

apps/web/src/components/dashboard/AppSidebar.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client"
22

3+
import { useEffect, useState } from "react"
34
import {
45
Sidebar,
56
SidebarContent,
@@ -10,10 +11,25 @@ import {
1011
import { NavGroup } from "./NavGroup"
1112
import { NavUser } from "./NavUser"
1213
import { TeamSwitcher } from "./TeamSwitcher"
13-
import { useSidebarData } from "./sidebar-data"
14+
import { useSidebarData, sidebarData as staticSidebarData } from "./sidebar-data"
1415

1516
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
16-
const sidebarData = useSidebarData()
17+
const [isClient, setIsClient] = useState(false)
18+
const [sidebarData, setSidebarData] = useState(staticSidebarData)
19+
20+
useEffect(() => {
21+
setIsClient(true)
22+
}, [])
23+
24+
// Only use the hook on client side to avoid SSR issues with Clerk
25+
if (isClient) {
26+
try {
27+
const data = useSidebarData()
28+
setSidebarData(data)
29+
} catch (error) {
30+
// If hook fails (e.g., Clerk not available), use static data
31+
}
32+
}
1733

1834
return (
1935
<div className="relative">

apps/web/src/components/dashboard/DashboardLayout.tsx

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { ReactNode } from "react"
44
import { SignedIn, SignedOut } from "@clerk/clerk-react"
55
import { useNavigate } from "@tanstack/react-router"
6-
import { useEffect } from "react"
6+
import { useEffect, useState } from "react"
77
import { cn } from "@/lib/utils"
88
import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar"
99
import { SkipLink } from "@/components/ui/skip-link"
@@ -14,6 +14,8 @@ interface DashboardLayoutProps {
1414
children: ReactNode
1515
}
1616

17+
const clerkPublishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
18+
1719
function RedirectToLogin() {
1820
const navigate = useNavigate()
1921

@@ -25,6 +27,45 @@ function RedirectToLogin() {
2527
}
2628

2729
export function DashboardLayout({ children }: DashboardLayoutProps) {
30+
const [isClient, setIsClient] = useState(false)
31+
32+
useEffect(() => {
33+
setIsClient(true)
34+
}, [])
35+
36+
// If Clerk is not configured, render dashboard without auth checks
37+
if (!clerkPublishableKey) {
38+
return (
39+
<>
40+
<SkipLink />
41+
<div className="border-grid flex flex-1 flex-col">
42+
<SidebarProvider defaultOpen={true}>
43+
<AppSidebar />
44+
<SidebarInset>
45+
<Header />
46+
<div
47+
id="content"
48+
className={cn(
49+
"flex h-full w-full flex-col",
50+
"has-[div[data-layout=fixed]]:h-svh",
51+
"group-data-[scroll-locked=1]/body:h-full",
52+
"has-[data-layout=fixed]:group-data-[scroll-locked=1]/body:h-svh"
53+
)}
54+
>
55+
{children}
56+
</div>
57+
</SidebarInset>
58+
</SidebarProvider>
59+
</div>
60+
</>
61+
)
62+
}
63+
64+
// Only render Clerk components on client side
65+
if (!isClient) {
66+
return null
67+
}
68+
2869
return (
2970
<>
3071
<SignedOut>

apps/web/src/components/dashboard/Search.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
CommandList,
1515
CommandSeparator,
1616
} from "@/components/ui/command"
17-
import { useSidebarData } from "./sidebar-data"
17+
import { sidebarData as staticSidebarData } from "./sidebar-data"
1818

1919
interface Props {
2020
className?: string
@@ -24,7 +24,10 @@ interface Props {
2424
export function Search({ className = "", placeholder = "Search" }: Props) {
2525
const [open, setOpen] = React.useState(false)
2626
const navigate = useNavigate()
27-
const sidebarData = useSidebarData()
27+
28+
// Use static sidebar data to avoid ConvexProvider errors
29+
// The Search component doesn't need real-time data, static navigation is sufficient
30+
const sidebarData = staticSidebarData
2831

2932
// Flatten all navigation items for the command palette
3033
const allNavItems = React.useMemo(() => {

apps/web/src/components/dashboard/TeamSwitcher.tsx

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import * as React from "react"
44
import { createPortal } from "react-dom"
55
import { ChevronsUpDown, Plus } from "lucide-react"
66
import { cn } from "@/lib/utils"
7-
import { useOrganizationList } from "@clerk/clerk-react"
87
import {
98
DropdownMenu,
109
DropdownMenuContent,
@@ -22,6 +21,20 @@ import {
2221
} from "@/components/ui/sidebar"
2322
import { Building2 } from "lucide-react"
2423

24+
const clerkPublishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
25+
26+
// Conditionally import useOrganizationList - only if Clerk is configured
27+
let useOrganizationListHook: (() => { userMemberships: any; setActive: any }) | null = null
28+
if (clerkPublishableKey) {
29+
try {
30+
// eslint-disable-next-line @typescript-eslint/no-var-requires
31+
const clerkReact = require("@clerk/clerk-react")
32+
useOrganizationListHook = clerkReact.useOrganizationList
33+
} catch {
34+
// Clerk not available
35+
}
36+
}
37+
2538
interface Props {
2639
teams: {
2740
name: string
@@ -32,10 +45,27 @@ interface Props {
3245

3346
export function TeamSwitcher({ teams }: Props) {
3447
const { isMobile } = useSidebar()
35-
const { userMemberships, setActive } = useOrganizationList()
3648
const [activeTeam, setActiveTeam] = React.useState<typeof teams[0] | undefined>(teams[0])
3749
const [open, setOpen] = React.useState(false)
3850

51+
// Conditionally use Clerk organizations if available
52+
// Note: This hook should only be called on client side where ClerkProvider is available
53+
// AppSidebar ensures this component only renders on client
54+
let userMemberships: { data?: Array<{ organization: { name: string; id: string; membersCount?: number } }> } | null = null
55+
let setActive: ((params: { organization: string }) => Promise<void>) | null = null
56+
57+
if (useOrganizationListHook && typeof window !== "undefined") {
58+
try {
59+
// Always call the hook if it's available (React rules require unconditional calls)
60+
// This will throw if not in ClerkProvider, but AppSidebar ensures we're on client
61+
const orgList = useOrganizationListHook()
62+
userMemberships = orgList.userMemberships
63+
setActive = orgList.setActive
64+
} catch {
65+
// Clerk not available (e.g., not wrapped in ClerkProvider), use teams prop
66+
}
67+
}
68+
3969
if (!activeTeam) {
4070
return null
4171
}

apps/web/src/components/dashboard/sidebar-data.tsx

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,25 +29,80 @@ import {
2929
Inbox,
3030
} from "lucide-react"
3131
import { type SidebarData } from "./types"
32-
import { useUser } from "@clerk/clerk-react"
3332
import { useQuery } from "convex/react"
3433
import { api } from "convex/_generated/api"
3534
import type { Doc } from "convex/_generated/dataModel"
3635

36+
const clerkPublishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
37+
38+
// Conditionally import useUser - only if Clerk is configured
39+
// This avoids bundling Clerk if not needed, but we must handle SSR
40+
let useUserHook: (() => { user: any } | null) | null = null
41+
if (clerkPublishableKey && typeof window !== "undefined") {
42+
try {
43+
// eslint-disable-next-line @typescript-eslint/no-var-requires
44+
const clerkReact = require("@clerk/clerk-react")
45+
useUserHook = clerkReact.useUser
46+
} catch {
47+
// Clerk not available
48+
}
49+
}
50+
3751
// Helper hook to get user data from Clerk
52+
// Note: This hook should only be called on the client side (after mount)
53+
// to avoid SSR issues with ClerkProvider. AppSidebar ensures this.
3854
function useSidebarUser() {
39-
const { user } = useUser()
55+
// Check if Convex is configured before calling useQuery
56+
const convexUrl = import.meta.env.VITE_CONVEX_URL
57+
if (!convexUrl) {
58+
return {
59+
name: "User",
60+
email: "",
61+
avatar: "",
62+
}
63+
}
64+
65+
// useQuery will throw if ConvexProvider isn't available
66+
// We can't catch this, so the caller must handle it
4067
const convexUser = useQuery(api.users.getCurrent)
4168

69+
// If Clerk is not configured or not available, only use Convex user
70+
if (!useUserHook) {
71+
return {
72+
name: convexUser?.name || "User",
73+
email: "",
74+
avatar: "",
75+
}
76+
}
77+
78+
// Always call the hook if it's available (React rules require unconditional calls)
79+
// This will throw if not in ClerkProvider, but AppSidebar only calls this on client
80+
// where ClerkProvider should be available
81+
const { user: clerkUser } = useUserHook()
82+
4283
return {
43-
name: user?.firstName || convexUser?.name || user?.fullName || user?.primaryEmailAddress?.emailAddress?.split("@")[0] || "User",
44-
email: user?.primaryEmailAddress?.emailAddress || "",
45-
avatar: user?.imageUrl || "",
84+
name: clerkUser?.firstName || convexUser?.name || clerkUser?.fullName || clerkUser?.primaryEmailAddress?.emailAddress?.split("@")[0] || "User",
85+
email: clerkUser?.primaryEmailAddress?.emailAddress || "",
86+
avatar: clerkUser?.imageUrl || "",
4687
}
4788
}
4889

4990
// Helper hook to get organizations from Clerk
5091
function useSidebarTeams() {
92+
// Check if Convex is configured before calling useQuery
93+
const convexUrl = import.meta.env.VITE_CONVEX_URL
94+
if (!convexUrl) {
95+
return [
96+
{
97+
name: "StackDock",
98+
logo: Building2,
99+
plan: "Free",
100+
},
101+
]
102+
}
103+
104+
// useQuery will throw if ConvexProvider isn't available
105+
// We can't catch this, so the caller must handle it
51106
const organizations = useQuery(api.organizations.list)
52107

53108
if (!organizations || organizations.length === 0) {
@@ -68,6 +123,17 @@ function useSidebarTeams() {
68123
}
69124

70125
export function useSidebarData(): SidebarData {
126+
// Check if Convex is configured
127+
const convexUrl = import.meta.env.VITE_CONVEX_URL
128+
129+
// If Convex is not configured, return static data
130+
if (!convexUrl) {
131+
return sidebarData
132+
}
133+
134+
// Try to use hooks, but fall back to static data if ConvexProvider isn't available
135+
// Note: We can't wrap hooks in try-catch, so if useQuery throws, it will propagate
136+
// The component using this hook should handle the error or ensure ConvexProvider is available
71137
const user = useSidebarUser()
72138
const teams = useSidebarTeams()
73139

apps/web/src/lib/convex-clerk.tsx

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,42 @@ function ConvexWithClerk({ children }: { children: ReactNode }) {
4040

4141
/**
4242
* Root provider that wraps Clerk and Convex
43-
* Only renders providers if both are configured
43+
* Always renders ConvexProvider to avoid "Could not find Convex client" errors
44+
* If Convex URL is not configured, creates a client that will handle errors gracefully
4445
*/
4546
export function ConvexClerkProvider({ children }: { children: ReactNode }) {
46-
// If Convex is not configured, skip providers
47-
if (!convexUrl) {
47+
// Always create a Convex client to ensure ConvexProvider is always available
48+
// This prevents "Could not find Convex client" errors in components
49+
const convex = useMemo(() => {
50+
if (convexUrl) {
51+
return new ConvexReactClient(convexUrl)
52+
}
53+
// If URL is not configured, create a client with a dummy URL
54+
// Queries will fail gracefully (return undefined) instead of crashing
55+
// This allows components to use useQuery without "Could not find Convex client" errors
56+
try {
57+
return new ConvexReactClient("https://unconfigured.convex.cloud")
58+
} catch {
59+
// If client creation fails, return null and handle below
60+
return null
61+
}
62+
}, [convexUrl])
63+
64+
// If Convex client couldn't be created, render children without ConvexProvider
65+
// This should only happen in edge cases
66+
if (!convex) {
4867
return <>{children}</>
4968
}
5069

70+
// If Convex is not configured, still render ConvexProvider with dummy client
71+
// This prevents "Could not find Convex client" errors in components
72+
// Components should check if data is undefined and handle accordingly
73+
if (!convexUrl) {
74+
return <ConvexProvider client={convex}>{children}</ConvexProvider>
75+
}
76+
5177
// If Clerk is not configured, use Convex without auth
5278
if (!clerkPublishableKey) {
53-
const convex = new ConvexReactClient(convexUrl)
5479
return <ConvexProvider client={convex}>{children}</ConvexProvider>
5580
}
5681

@@ -61,4 +86,3 @@ export function ConvexClerkProvider({ children }: { children: ReactNode }) {
6186
</ClerkProvider>
6287
)
6388
}
64-

0 commit comments

Comments
 (0)