create-aron-app 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -31
- package/dist/index.js +38 -49
- package/package.json +3 -3
- package/templates/.cursor/rules/backend.mdc +112 -0
- package/templates/.cursor/rules/coding_standards.mdc +85 -4
- package/templates/.cursor/rules/frontend_architecture.mdc +334 -0
- package/templates/.github/workflows/ci.yml +17 -6
- package/templates/apps/{react-router → web}/.react-router/types/+routes.ts +11 -6
- package/templates/apps/{react-router/.react-router/types/src/routes/(dashboard)/todos → web/.react-router/types/src/routes/(dashboard)/(todos)}/+types/[id].ts +5 -2
- package/templates/apps/{react-router/.react-router/types/src/routes/(dashboard)/todos → web/.react-router/types/src/routes/(dashboard)/(todos)}/+types/index.ts +5 -2
- package/templates/apps/web/.react-router/types/src/routes/(dashboard)/(todos)/+types/layout.ts +65 -0
- package/templates/apps/{react-router → web}/project.json +11 -4
- package/templates/apps/{react-router → web}/react-router.config.ts +1 -1
- package/templates/apps/web/src/libs/convex_query_client.ts +11 -0
- package/templates/apps/web/src/libs/react_query_client.ts +17 -0
- package/templates/apps/web/src/libs/server/auth.ts +32 -0
- package/templates/apps/web/src/libs/server/protected.ts +17 -0
- package/templates/apps/web/src/providers/api_auth_provider.tsx +26 -0
- package/templates/apps/web/src/providers/global_provider.tsx +28 -0
- package/templates/apps/web/src/providers/navigation_loading_bar_provider.tsx +72 -0
- package/templates/apps/web/src/root.tsx +68 -0
- package/templates/apps/web/src/routes/(dashboard)/(todos)/[id].tsx +33 -0
- package/templates/apps/web/src/routes/(dashboard)/(todos)/index.tsx +26 -0
- package/templates/apps/web/src/routes/(dashboard)/(todos)/layout.tsx +9 -0
- package/templates/apps/{react-router → web}/src/routes/(dashboard)/index.tsx +3 -2
- package/templates/apps/web/src/routes/(dashboard)/layout.tsx +20 -0
- package/templates/apps/{react-router → web}/src/routes.ts +4 -2
- package/templates/apps/{nextjs → web}/src/surfaces/sidebar/install.tsx +1 -5
- package/templates/apps/{react-router → web}/src/surfaces/sidebar/layout.tsx +24 -15
- package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/all_todos.tsx +1 -1
- package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/all_todos_controller.ts +1 -1
- package/templates/apps/web/src/surfaces/todos/all_todos/bootstrap.ts +21 -0
- package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/bootstrap.ts +4 -5
- package/templates/apps/{nextjs → web}/src/surfaces/todos/single_todo/header/create.tsx +4 -6
- package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/single_todo_controller.ts +1 -1
- package/templates/biome.json +5 -0
- package/templates/bun.lock +11 -1281
- package/templates/nx.json +0 -11
- package/templates/package.json +2 -3
- package/templates/shared/assets/src/styles/global.css +14 -8
- package/templates/shared/ui/src/base/collapsible.tsx +31 -0
- package/templates/shared/ui/src/base/hover-card.tsx +42 -0
- package/templates/shared/ui/src/base/input-group.tsx +168 -0
- package/templates/shared/ui/src/base/panel.tsx +93 -0
- package/templates/shared/ui/src/hooks/use_mobile.tsx +1 -1
- package/templates/shared/ui/src/hooks/use_query_params.tsx +6 -7
- package/templates/shared/ui/tsconfig.json +1 -1
- package/templates/shared/utils/src/convex.ts +2 -1
- package/templates/tsconfig.base.json +2 -4
- package/templates/.cursor/commands/builder.md +0 -0
- package/templates/.cursor/rules/api_architecture.mdc +0 -262
- package/templates/.cursor/rules/convex_rules.mdc +0 -331
- package/templates/.cursor/rules/frontend_architecture_core.mdc +0 -495
- package/templates/.cursor/rules/frontend_architecture_nextjs.mdc +0 -458
- package/templates/.cursor/rules/frontend_architecture_reactrouter.mdc +0 -473
- package/templates/apps/nextjs/.env.example +0 -10
- package/templates/apps/nextjs/index.d.ts +0 -6
- package/templates/apps/nextjs/next-env.d.ts +0 -5
- package/templates/apps/nextjs/next.config.js +0 -22
- package/templates/apps/nextjs/postcss.config.js +0 -17
- package/templates/apps/nextjs/project.json +0 -22
- package/templates/apps/nextjs/src/app/(auth)/layout.tsx +0 -21
- package/templates/apps/nextjs/src/app/(auth)/not-allowed/page.tsx +0 -23
- package/templates/apps/nextjs/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +0 -15
- package/templates/apps/nextjs/src/app/(dashboard)/layout.tsx +0 -22
- package/templates/apps/nextjs/src/app/(dashboard)/page.tsx +0 -12
- package/templates/apps/nextjs/src/app/(dashboard)/todos/[id]/page.tsx +0 -26
- package/templates/apps/nextjs/src/app/(dashboard)/todos/page.tsx +0 -19
- package/templates/apps/nextjs/src/app/app.css +0 -3
- package/templates/apps/nextjs/src/app/layout.tsx +0 -26
- package/templates/apps/nextjs/src/middleware.ts +0 -18
- package/templates/apps/nextjs/src/providers/convex_provider.tsx +0 -44
- package/templates/apps/nextjs/src/surfaces/home/home.tsx +0 -27
- package/templates/apps/nextjs/src/surfaces/home/layout.tsx +0 -44
- package/templates/apps/nextjs/src/surfaces/home/main/create.tsx +0 -34
- package/templates/apps/nextjs/src/surfaces/sidebar/layout.tsx +0 -118
- package/templates/apps/nextjs/src/surfaces/sidebar/nav_main/create.tsx +0 -19
- package/templates/apps/nextjs/src/surfaces/sidebar/nav_main/nav_config.ts +0 -22
- package/templates/apps/nextjs/src/surfaces/sidebar/nav_main/nav_main.tsx +0 -25
- package/templates/apps/nextjs/src/surfaces/sidebar/nav_secondary/create.tsx +0 -21
- package/templates/apps/nextjs/src/surfaces/sidebar/nav_secondary/nav_secondary.tsx +0 -33
- package/templates/apps/nextjs/src/surfaces/sidebar/sidebar.tsx +0 -23
- package/templates/apps/nextjs/src/surfaces/sidebar/ui/sidebar_nav_link.tsx +0 -39
- package/templates/apps/nextjs/src/surfaces/sidebar/user_menu/create.tsx +0 -28
- package/templates/apps/nextjs/src/surfaces/sidebar/user_menu/user_menu.tsx +0 -42
- package/templates/apps/nextjs/src/surfaces/todos/all_todos/all_todos.tsx +0 -29
- package/templates/apps/nextjs/src/surfaces/todos/all_todos/all_todos_controller.ts +0 -61
- package/templates/apps/nextjs/src/surfaces/todos/all_todos/bootstrap.ts +0 -21
- package/templates/apps/nextjs/src/surfaces/todos/all_todos/header/create.tsx +0 -23
- package/templates/apps/nextjs/src/surfaces/todos/all_todos/install.tsx +0 -23
- package/templates/apps/nextjs/src/surfaces/todos/all_todos/layout.tsx +0 -44
- package/templates/apps/nextjs/src/surfaces/todos/all_todos/main/create.tsx +0 -49
- package/templates/apps/nextjs/src/surfaces/todos/all_todos/main/main.tsx +0 -70
- package/templates/apps/nextjs/src/surfaces/todos/all_todos/main/new_todo_sheet/create.tsx +0 -56
- package/templates/apps/nextjs/src/surfaces/todos/all_todos/main/new_todo_sheet/new_todo_sheet.tsx +0 -99
- package/templates/apps/nextjs/src/surfaces/todos/single_todo/bootstrap.ts +0 -32
- package/templates/apps/nextjs/src/surfaces/todos/single_todo/header/header.tsx +0 -22
- package/templates/apps/nextjs/src/surfaces/todos/single_todo/install.tsx +0 -27
- package/templates/apps/nextjs/src/surfaces/todos/single_todo/layout.tsx +0 -55
- package/templates/apps/nextjs/src/surfaces/todos/single_todo/main/create.tsx +0 -38
- package/templates/apps/nextjs/src/surfaces/todos/single_todo/main/main.tsx +0 -49
- package/templates/apps/nextjs/src/surfaces/todos/single_todo/single_todo.tsx +0 -29
- package/templates/apps/nextjs/src/surfaces/todos/single_todo/single_todo_controller.ts +0 -13
- package/templates/apps/nextjs/src/utils/auth.ts +0 -18
- package/templates/apps/nextjs/src/utils/convex.ts +0 -11
- package/templates/apps/nextjs/src/utils/font.ts +0 -9
- package/templates/apps/nextjs/tsconfig.json +0 -42
- package/templates/apps/react-router/src/providers/api_auth_provider.tsx +0 -40
- package/templates/apps/react-router/src/root.tsx +0 -37
- package/templates/apps/react-router/src/routes/(dashboard)/layout.tsx +0 -37
- package/templates/apps/react-router/src/routes/(dashboard)/todos/[id].tsx +0 -19
- package/templates/apps/react-router/src/routes/(dashboard)/todos/index.tsx +0 -19
- package/templates/apps/react-router/src/surfaces/home/bootstrap.ts +0 -9
- package/templates/apps/react-router/src/surfaces/home/install.tsx +0 -17
- package/templates/apps/react-router/src/surfaces/sidebar/install.tsx +0 -23
- package/templates/apps/react-router/src/surfaces/sidebar/nav_secondary/create.tsx +0 -21
- package/templates/apps/react-router/src/surfaces/sidebar/nav_secondary/nav_secondary.tsx +0 -31
- package/templates/apps/react-router/src/surfaces/todos/all_todos/bootstrap.ts +0 -18
- package/templates/apps/react-router/src/surfaces/todos/all_todos/main/new_todo_sheet/schema.ts +0 -11
- package/templates/apps/react-router/src/surfaces/todos/single_todo/header/create.tsx +0 -32
- /package/templates/apps/{react-router → web}/.env.example +0 -0
- /package/templates/apps/{react-router → web}/.react-router/types/+future.ts +0 -0
- /package/templates/apps/{react-router → web}/.react-router/types/+server-build.d.ts +0 -0
- /package/templates/apps/{react-router → web}/.react-router/types/src/+types/root.ts +0 -0
- /package/templates/apps/{react-router → web}/.react-router/types/src/routes/(auth)/+types/layout.ts +0 -0
- /package/templates/apps/{react-router → web}/.react-router/types/src/routes/(auth)/sign-in/+types/index.ts +0 -0
- /package/templates/apps/{react-router → web}/.react-router/types/src/routes/(dashboard)/+types/index.ts +0 -0
- /package/templates/apps/{react-router → web}/.react-router/types/src/routes/(dashboard)/+types/layout.ts +0 -0
- /package/templates/apps/{react-router → web}/postcss.config.js +0 -0
- /package/templates/apps/{react-router → web}/public/favicon.ico +0 -0
- /package/templates/apps/{react-router → web}/src/app.css +0 -0
- /package/templates/apps/{react-router → web}/src/components/error_boundary.tsx +0 -0
- /package/templates/apps/{react-router → web}/src/routes/(auth)/layout.tsx +0 -0
- /package/templates/apps/{react-router → web}/src/routes/(auth)/sign-in/index.tsx +0 -0
- /package/templates/apps/{nextjs → web}/src/surfaces/home/bootstrap.ts +0 -0
- /package/templates/apps/{react-router → web}/src/surfaces/home/home.tsx +0 -0
- /package/templates/apps/{nextjs → web}/src/surfaces/home/install.tsx +0 -0
- /package/templates/apps/{react-router → web}/src/surfaces/home/layout.tsx +0 -0
- /package/templates/apps/{react-router → web}/src/surfaces/home/main/create.tsx +0 -0
- /package/templates/apps/{react-router → web}/src/surfaces/sidebar/nav_main/create.tsx +0 -0
- /package/templates/apps/{react-router → web}/src/surfaces/sidebar/nav_main/nav_main.tsx +0 -0
- /package/templates/apps/{react-router → web}/src/surfaces/sidebar/sidebar.tsx +0 -0
- /package/templates/apps/{react-router → web}/src/surfaces/sidebar/user_menu/create.tsx +0 -0
- /package/templates/apps/{react-router → web}/src/surfaces/sidebar/user_menu/user_menu.tsx +0 -0
- /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/header/create.tsx +0 -0
- /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/install.tsx +0 -0
- /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/layout.tsx +0 -0
- /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/main/create.tsx +0 -0
- /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/main/main.tsx +0 -0
- /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/main/new_todo_sheet/create.tsx +0 -0
- /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/main/new_todo_sheet/new_todo_sheet.tsx +0 -0
- /package/templates/apps/{nextjs → web}/src/surfaces/todos/all_todos/main/new_todo_sheet/schema.ts +0 -0
- /package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/header/header.tsx +0 -0
- /package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/install.tsx +0 -0
- /package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/layout.tsx +0 -0
- /package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/main/create.tsx +0 -0
- /package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/main/main.tsx +0 -0
- /package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/single_todo.tsx +0 -0
- /package/templates/apps/{react-router → web}/tsconfig.json +0 -0
- /package/templates/apps/{react-router → web}/vite.config.ts +0 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ConvexQueryClient } from "@convex-dev/react-query";
|
|
2
|
+
import { ConvexReactClient } from "convex/react";
|
|
3
|
+
|
|
4
|
+
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string, {
|
|
5
|
+
expectAuth: true,
|
|
6
|
+
logger: true,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const convexQueryClient = new ConvexQueryClient(convex);
|
|
10
|
+
|
|
11
|
+
export { convex, convexQueryClient };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
2
|
+
|
|
3
|
+
import { convexQueryClient } from "@/web/libs/convex_query_client";
|
|
4
|
+
|
|
5
|
+
const reactQueryClient = new QueryClient({
|
|
6
|
+
defaultOptions: {
|
|
7
|
+
queries: {
|
|
8
|
+
queryFn: convexQueryClient.queryFn(),
|
|
9
|
+
gcTime: 0,
|
|
10
|
+
staleTime: 0,
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
convexQueryClient.connect(reactQueryClient);
|
|
16
|
+
|
|
17
|
+
export { reactQueryClient };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Aron Weston 2026.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getAuth } from "@clerk/react-router/server";
|
|
6
|
+
import { ConvexHttpClient } from "convex/browser";
|
|
7
|
+
import type { LoaderFunctionArgs } from "react-router";
|
|
8
|
+
import { redirect } from "react-router";
|
|
9
|
+
|
|
10
|
+
const CONVEX_URL = process.env.VITE_CONVEX_URL;
|
|
11
|
+
|
|
12
|
+
if (!CONVEX_URL) {
|
|
13
|
+
throw new Error("VITE_CONVEX_URL is not set");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const createConvexClient = async (args: LoaderFunctionArgs): Promise<ConvexHttpClient> => {
|
|
17
|
+
try {
|
|
18
|
+
const auth = await getAuth(args);
|
|
19
|
+
if (!auth.isAuthenticated) {
|
|
20
|
+
throw redirect(`/sign-in?redirect_url=${encodeURIComponent(args.request.url)}`);
|
|
21
|
+
}
|
|
22
|
+
const token = await auth.getToken({ template: "convex" });
|
|
23
|
+
const client = new ConvexHttpClient(CONVEX_URL);
|
|
24
|
+
if (token) {
|
|
25
|
+
client.setAuth(token);
|
|
26
|
+
}
|
|
27
|
+
return client;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error(error);
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getAuth } from "@clerk/react-router/server";
|
|
2
|
+
import { type MiddlewareFunction, redirect } from "react-router";
|
|
3
|
+
|
|
4
|
+
const PUBLIC_ROUTES = ["/sign-in"];
|
|
5
|
+
|
|
6
|
+
export const protectedRoute: MiddlewareFunction<Response> = async ({ request, context }, next) => {
|
|
7
|
+
const { pathname } = new URL(request.url);
|
|
8
|
+
const isPublic = PUBLIC_ROUTES.some((route) => pathname.startsWith(route));
|
|
9
|
+
|
|
10
|
+
if (!isPublic) {
|
|
11
|
+
const auth = await getAuth({ request, context } as Parameters<typeof getAuth>[0]);
|
|
12
|
+
if (!auth.isAuthenticated) {
|
|
13
|
+
throw redirect(`/sign-in?redirect_url=${encodeURIComponent(request.url)}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return next();
|
|
17
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Aron Weston 2026.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ClerkProvider, useAuth } from "@clerk/react-router";
|
|
6
|
+
import { QueryClientProvider } from "@tanstack/react-query";
|
|
7
|
+
import type { Route } from "apps/web/.react-router/types/src/+types/root";
|
|
8
|
+
import { ConvexProviderWithClerk } from "convex/react-clerk";
|
|
9
|
+
|
|
10
|
+
import { convex } from "@/web/libs/convex_query_client";
|
|
11
|
+
import { reactQueryClient } from "@/web/libs/react_query_client";
|
|
12
|
+
|
|
13
|
+
type ApiAuthProviderProps = {
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
loaderData: Route.ComponentProps["loaderData"];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const ApiAuthProvider = ({ children, loaderData }: ApiAuthProviderProps) => {
|
|
19
|
+
return (
|
|
20
|
+
<ClerkProvider signInUrl="/sign-in" loaderData={loaderData}>
|
|
21
|
+
<ConvexProviderWithClerk client={convex} useAuth={useAuth}>
|
|
22
|
+
<QueryClientProvider client={reactQueryClient}>{children}</QueryClientProvider>
|
|
23
|
+
</ConvexProviderWithClerk>
|
|
24
|
+
</ClerkProvider>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Aron Weston 2026.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ReactNode } from "react";
|
|
6
|
+
import { createContext, useContext } from "react";
|
|
7
|
+
|
|
8
|
+
export type GlobalContextValue = Record<string, never>;
|
|
9
|
+
|
|
10
|
+
const GlobalContext = createContext<GlobalContextValue | undefined>(undefined);
|
|
11
|
+
|
|
12
|
+
type GlobalProviderProps = {
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const GlobalProvider = ({ children }: GlobalProviderProps) => {
|
|
17
|
+
return <GlobalContext.Provider value={{}}>{children}</GlobalContext.Provider>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const useGlobal = (): GlobalContextValue => {
|
|
21
|
+
const context = useContext(GlobalContext);
|
|
22
|
+
|
|
23
|
+
if (context === undefined) {
|
|
24
|
+
throw new Error("useGlobal must be used within a GlobalProvider");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return context;
|
|
28
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Aron Weston 2026.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createContext, type ReactNode, useContext, useEffect, useMemo, useRef } from "react";
|
|
6
|
+
import { useNavigation } from "react-router";
|
|
7
|
+
import { LoadingBarContainer, useLoadingBar } from "react-top-loading-bar";
|
|
8
|
+
|
|
9
|
+
export type NavigationLoadingBarApi = {
|
|
10
|
+
start: () => void;
|
|
11
|
+
complete: () => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const NavigationLoadingBarContext = createContext<NavigationLoadingBarApi | null>(null);
|
|
15
|
+
|
|
16
|
+
type NavigationLoadingBarProviderProps = {
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const NavigationLoadingBarInner = ({ children }: NavigationLoadingBarProviderProps) => {
|
|
21
|
+
const navigation = useNavigation();
|
|
22
|
+
|
|
23
|
+
const { start, complete } = useLoadingBar({
|
|
24
|
+
color: "hsl(var(--primary))",
|
|
25
|
+
height: 2,
|
|
26
|
+
shadow: true,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const api = useMemo((): NavigationLoadingBarApi => {
|
|
30
|
+
return { start, complete };
|
|
31
|
+
}, [start, complete]);
|
|
32
|
+
|
|
33
|
+
const skippedInitialIdle = useRef(false);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (navigation.state !== "idle") {
|
|
37
|
+
start();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!skippedInitialIdle.current) {
|
|
42
|
+
skippedInitialIdle.current = true;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
complete();
|
|
47
|
+
}, [navigation.state, start, complete]);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<NavigationLoadingBarContext.Provider value={api}>
|
|
51
|
+
{children}
|
|
52
|
+
</NavigationLoadingBarContext.Provider>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const NavigationLoadingBarProvider = ({ children }: NavigationLoadingBarProviderProps) => {
|
|
57
|
+
return (
|
|
58
|
+
<LoadingBarContainer>
|
|
59
|
+
<NavigationLoadingBarInner>{children}</NavigationLoadingBarInner>
|
|
60
|
+
</LoadingBarContainer>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const useNavigationLoadingBar = (): NavigationLoadingBarApi => {
|
|
65
|
+
const ctx = useContext(NavigationLoadingBarContext);
|
|
66
|
+
|
|
67
|
+
if (!ctx) {
|
|
68
|
+
throw new Error("useNavigationLoadingBar must be used within NavigationLoadingBarProvider");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return ctx;
|
|
72
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Aron Weston 2026.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import "./app.css";
|
|
6
|
+
|
|
7
|
+
import { clerkMiddleware, rootAuthLoader } from "@clerk/react-router/server";
|
|
8
|
+
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
|
|
9
|
+
import { Toaster } from "sonner";
|
|
10
|
+
|
|
11
|
+
import { protectedRoute } from "@/web/libs/server/protected";
|
|
12
|
+
import { ApiAuthProvider } from "@/web/providers/api_auth_provider";
|
|
13
|
+
import { GlobalProvider } from "@/web/providers/global_provider";
|
|
14
|
+
import { NavigationLoadingBarProvider } from "@/web/providers/navigation_loading_bar_provider";
|
|
15
|
+
|
|
16
|
+
import type { Route } from "./+types/root";
|
|
17
|
+
|
|
18
|
+
export const middleware: Route.MiddlewareFunction[] = [clerkMiddleware(), protectedRoute];
|
|
19
|
+
export const loader = (args: Route.LoaderArgs) => rootAuthLoader(args);
|
|
20
|
+
|
|
21
|
+
export const links: Route.LinksFunction = () => {
|
|
22
|
+
return [
|
|
23
|
+
{
|
|
24
|
+
rel: "preconnect",
|
|
25
|
+
href: "https://fonts.googleapis.com",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
rel: "preconnect",
|
|
29
|
+
href: "https://fonts.gstatic.com",
|
|
30
|
+
crossOrigin: "anonymous",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
rel: "stylesheet",
|
|
34
|
+
href: "https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&display=swap",
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function Layout({ children }: { children: React.ReactNode }) {
|
|
40
|
+
return (
|
|
41
|
+
<html lang="en">
|
|
42
|
+
<head>
|
|
43
|
+
<meta charSet="utf-8" />
|
|
44
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
45
|
+
<Meta />
|
|
46
|
+
<Links />
|
|
47
|
+
</head>
|
|
48
|
+
<body>
|
|
49
|
+
{children}
|
|
50
|
+
<ScrollRestoration />
|
|
51
|
+
<Scripts />
|
|
52
|
+
</body>
|
|
53
|
+
</html>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default function App({ loaderData }: Route.ComponentProps) {
|
|
58
|
+
return (
|
|
59
|
+
<NavigationLoadingBarProvider>
|
|
60
|
+
<ApiAuthProvider loaderData={loaderData}>
|
|
61
|
+
<GlobalProvider>
|
|
62
|
+
<Outlet />
|
|
63
|
+
<Toaster richColors position="top-center" />
|
|
64
|
+
</GlobalProvider>
|
|
65
|
+
</ApiAuthProvider>
|
|
66
|
+
</NavigationLoadingBarProvider>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Aron Weston 2026.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createConvexClient } from "@/web/libs/server/auth";
|
|
6
|
+
import { bootstrapSingleTodo } from "@/web/surfaces/todos/single_todo/bootstrap";
|
|
7
|
+
import { SingleTodo } from "@/web/surfaces/todos/single_todo/single_todo";
|
|
8
|
+
|
|
9
|
+
import type { Route } from "./+types/[id]";
|
|
10
|
+
|
|
11
|
+
export async function loader(args: Route.LoaderArgs) {
|
|
12
|
+
const client = await createConvexClient(args);
|
|
13
|
+
return bootstrapSingleTodo({ params: args.params, client });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function clientLoader({ serverLoader }: Route.ClientLoaderArgs) {
|
|
17
|
+
return serverLoader();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function meta({ loaderData: bootstrap }: Route.MetaArgs) {
|
|
21
|
+
if (!bootstrap) {
|
|
22
|
+
return [{ title: "Todo" }];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return [
|
|
26
|
+
{ title: `${bootstrap.todo.title} - Todos` },
|
|
27
|
+
{ name: "description", content: bootstrap.todo.description ?? "Todo details" },
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default function TodoPage({ loaderData: bootstrap }: Route.ComponentProps) {
|
|
32
|
+
return <SingleTodo bootstrap={bootstrap} />;
|
|
33
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Aron Weston 2026.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createConvexClient } from "@/web/libs/server/auth";
|
|
6
|
+
import { AllTodos } from "@/web/surfaces/todos/all_todos/all_todos";
|
|
7
|
+
import { bootstrapAllTodos } from "@/web/surfaces/todos/all_todos/bootstrap";
|
|
8
|
+
|
|
9
|
+
import type { Route } from "./+types/index";
|
|
10
|
+
|
|
11
|
+
export async function loader(args: Route.LoaderArgs) {
|
|
12
|
+
const client = await createConvexClient(args);
|
|
13
|
+
return bootstrapAllTodos({ client });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function clientLoader({ serverLoader }: Route.ClientLoaderArgs) {
|
|
17
|
+
return serverLoader();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function meta(_args: Route.MetaArgs) {
|
|
21
|
+
return [{ title: "Todos" }, { name: "description", content: "Manage your todos" }];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function TodosPage({ loaderData: bootstrap }: Route.ComponentProps) {
|
|
25
|
+
return <AllTodos bootstrap={bootstrap} />;
|
|
26
|
+
}
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Copyright (c) Aron Weston 2026.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { bootstrapHome } from "@/web/surfaces/home/bootstrap";
|
|
6
|
+
import { Home } from "@/web/surfaces/home/home";
|
|
7
|
+
|
|
5
8
|
import type { Route } from "./+types/index";
|
|
6
|
-
import { Home } from "../../surfaces/home/home";
|
|
7
|
-
import { bootstrapHome } from "../../surfaces/home/bootstrap";
|
|
8
9
|
|
|
9
10
|
export async function clientLoader(_args: Route.ClientLoaderArgs) {
|
|
10
11
|
return bootstrapHome();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Aron Weston 2026.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CSSProperties } from "react";
|
|
6
|
+
import { Outlet } from "react-router";
|
|
7
|
+
|
|
8
|
+
import { SidebarInset, SidebarProvider } from "@/ui/base/side_bar";
|
|
9
|
+
import { Sidebar } from "@/web/surfaces/sidebar/sidebar";
|
|
10
|
+
|
|
11
|
+
export default function DashboardLayout() {
|
|
12
|
+
return (
|
|
13
|
+
<SidebarProvider style={{ "--sidebar-width": "265px" } as CSSProperties} defaultOpen={true}>
|
|
14
|
+
<Sidebar />
|
|
15
|
+
<SidebarInset className="bg-white">
|
|
16
|
+
<Outlet />
|
|
17
|
+
</SidebarInset>
|
|
18
|
+
</SidebarProvider>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -5,8 +5,10 @@ export default [
|
|
|
5
5
|
layout("./routes/(dashboard)/layout.tsx", [
|
|
6
6
|
index("./routes/(dashboard)/index.tsx"),
|
|
7
7
|
...prefix("todos", [
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
layout("./routes/(dashboard)/(todos)/layout.tsx", [
|
|
9
|
+
index("./routes/(dashboard)/(todos)/index.tsx"),
|
|
10
|
+
route(":id", "./routes/(dashboard)/(todos)/[id].tsx"),
|
|
11
|
+
]),
|
|
10
12
|
]),
|
|
11
13
|
]),
|
|
12
14
|
] satisfies RouteConfig;
|
|
@@ -10,11 +10,7 @@ type InstallSidebarOpts = {
|
|
|
10
10
|
|
|
11
11
|
export const installSidebar = ({ layout }: InstallSidebarOpts) => {
|
|
12
12
|
import("@/web/surfaces/sidebar/nav_main/create").then(({ createNavMain }) => {
|
|
13
|
-
layout.
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
import("@/web/surfaces/sidebar/nav_secondary/create").then(({ createNavSecondary }) => {
|
|
17
|
-
layout.setNavSecondary(createNavSecondary());
|
|
13
|
+
layout.setNavLinks(createNavMain());
|
|
18
14
|
});
|
|
19
15
|
|
|
20
16
|
import("@/web/surfaces/sidebar/user_menu/create").then(({ createUserMenu }) => {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Copyright (c) Aron Weston 2026.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { Plus } from "lucide-react";
|
|
5
6
|
import { action, observable } from "mobx";
|
|
6
7
|
import { observer } from "mobx-react-lite";
|
|
7
8
|
import type { ComponentType } from "react";
|
|
@@ -20,22 +21,14 @@ import { Skeleton } from "@/ui/base/skeleton";
|
|
|
20
21
|
|
|
21
22
|
export class SidebarLayoutController {
|
|
22
23
|
@observable.ref
|
|
23
|
-
accessor
|
|
24
|
-
|
|
25
|
-
@observable.ref
|
|
26
|
-
accessor NavSecondary: ComponentType | undefined = undefined;
|
|
24
|
+
accessor NavLinks: ComponentType | undefined = undefined;
|
|
27
25
|
|
|
28
26
|
@observable.ref
|
|
29
27
|
accessor UserMenu: ComponentType | undefined = undefined;
|
|
30
28
|
|
|
31
29
|
@action
|
|
32
|
-
|
|
33
|
-
this.
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
@action
|
|
37
|
-
setNavSecondary(NavSecondary: ComponentType) {
|
|
38
|
-
this.NavSecondary = NavSecondary;
|
|
30
|
+
setNavLinks(NavLinks: ComponentType) {
|
|
31
|
+
this.NavLinks = NavLinks;
|
|
39
32
|
}
|
|
40
33
|
|
|
41
34
|
@action
|
|
@@ -81,8 +74,7 @@ export const createSidebarLayout = () => {
|
|
|
81
74
|
return {
|
|
82
75
|
layout,
|
|
83
76
|
Layout: observer(() => {
|
|
84
|
-
const
|
|
85
|
-
const NavSecondary = layout.NavSecondary;
|
|
77
|
+
const NavLinks = layout.NavLinks;
|
|
86
78
|
const UserMenu = layout.UserMenu;
|
|
87
79
|
|
|
88
80
|
return (
|
|
@@ -95,10 +87,27 @@ export const createSidebarLayout = () => {
|
|
|
95
87
|
<BrandLink />
|
|
96
88
|
</SidebarHeader>
|
|
97
89
|
<SidebarContent className="flex flex-1 flex-col gap-4 overflow-hidden px-2">
|
|
98
|
-
|
|
90
|
+
<SidebarMenu>
|
|
91
|
+
<SidebarMenuItem>
|
|
92
|
+
<SidebarMenuButton
|
|
93
|
+
size="sm"
|
|
94
|
+
tooltip="Create a new todo"
|
|
95
|
+
className="bg-sidebar-primary text-sidebar-primary-foreground hover:bg-sidebar-primary/90 active:bg-sidebar-primary/80 justify-center font-semibold shadow-sm"
|
|
96
|
+
asChild
|
|
97
|
+
>
|
|
98
|
+
<Link
|
|
99
|
+
to="/todos"
|
|
100
|
+
className="flex items-center gap-2"
|
|
101
|
+
>
|
|
102
|
+
<Plus className="size-4" />
|
|
103
|
+
<span>New Todo</span>
|
|
104
|
+
</Link>
|
|
105
|
+
</SidebarMenuButton>
|
|
106
|
+
</SidebarMenuItem>
|
|
107
|
+
</SidebarMenu>
|
|
99
108
|
</SidebarContent>
|
|
100
109
|
<SidebarFooter className="gap-2">
|
|
101
|
-
{
|
|
110
|
+
{NavLinks ? <NavLinks /> : <Skeleton className="h-10 bg-sidebar-accent" />}
|
|
102
111
|
{UserMenu ? <UserMenu /> : <Skeleton className="h-12 bg-sidebar-accent" />}
|
|
103
112
|
</SidebarFooter>
|
|
104
113
|
</BaseSidebar>
|
|
@@ -13,7 +13,7 @@ export type AllTodosProps = {
|
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
export const AllTodos = ({ bootstrap }: AllTodosProps) => {
|
|
16
|
-
const { layout, Layout } = useMemo(
|
|
16
|
+
const { layout, Layout } = useMemo(createLayout, []);
|
|
17
17
|
const installed = useRef(false);
|
|
18
18
|
|
|
19
19
|
if (!installed.current) {
|
package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/all_todos_controller.ts
RENAMED
|
@@ -6,7 +6,7 @@ import { action, observable } from "mobx";
|
|
|
6
6
|
|
|
7
7
|
import { api } from "@/api/_generated/api";
|
|
8
8
|
import type { TodoId } from "@/api/todos/types";
|
|
9
|
-
import { convex } from "@/web/
|
|
9
|
+
import { convex } from "@/web/libs/convex_query_client";
|
|
10
10
|
|
|
11
11
|
export class AllTodosController {
|
|
12
12
|
@observable
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Aron Weston 2026.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ConvexHttpClient } from "convex/browser";
|
|
6
|
+
|
|
7
|
+
import { api } from "@/api/_generated/api";
|
|
8
|
+
import type { Todo } from "@/api/todos/types";
|
|
9
|
+
|
|
10
|
+
export type AllTodosBootstrap = {
|
|
11
|
+
todos: Todo[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type AllTodosBootstrapArgs = {
|
|
15
|
+
client: ConvexHttpClient;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const bootstrapAllTodos = async ({ client }: AllTodosBootstrapArgs): Promise<AllTodosBootstrap> => {
|
|
19
|
+
const todos = await client.query(api.todos.crud.listTodos, {});
|
|
20
|
+
return { todos };
|
|
21
|
+
};
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
* Copyright (c) Aron Weston 2026.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import type { ConvexHttpClient } from "convex/browser";
|
|
6
6
|
import { redirect } from "react-router";
|
|
7
7
|
|
|
8
8
|
import { api } from "@/api/_generated/api";
|
|
9
9
|
import type { Todo, TodoId } from "@/api/todos/types";
|
|
10
|
-
import { queryClient } from "@/web/providers/api_auth_provider";
|
|
11
10
|
|
|
12
11
|
export type SingleTodoBootstrapArgs = {
|
|
13
12
|
params: { id?: string };
|
|
13
|
+
client: ConvexHttpClient;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
export type SingleTodoBootstrap = {
|
|
@@ -20,15 +20,14 @@ export type SingleTodoBootstrap = {
|
|
|
20
20
|
|
|
21
21
|
export const bootstrapSingleTodo = async ({
|
|
22
22
|
params,
|
|
23
|
+
client,
|
|
23
24
|
}: SingleTodoBootstrapArgs): Promise<SingleTodoBootstrap> => {
|
|
24
25
|
if (!params.id) {
|
|
25
26
|
throw redirect("/todos");
|
|
26
27
|
}
|
|
27
28
|
const todoId = params.id as TodoId;
|
|
28
29
|
try {
|
|
29
|
-
const todo = await
|
|
30
|
-
convexQuery(api.todos.crud.getTodo, { todoId }),
|
|
31
|
-
);
|
|
30
|
+
const todo = await client.query(api.todos.crud.getTodo, { todoId });
|
|
32
31
|
return { todoId, todo };
|
|
33
32
|
} catch {
|
|
34
33
|
throw redirect("/todos");
|
|
@@ -2,23 +2,21 @@
|
|
|
2
2
|
* Copyright (c) Aron Weston 2026.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
"use client";
|
|
6
|
-
|
|
7
5
|
import { observer } from "mobx-react-lite";
|
|
8
|
-
import type {
|
|
6
|
+
import type { NavigateFunction } from "react-router";
|
|
9
7
|
|
|
10
8
|
import { Header } from "@/web/surfaces/todos/single_todo/header/header";
|
|
11
9
|
|
|
12
10
|
export type CreateHeaderOpts = {
|
|
13
|
-
|
|
11
|
+
navigate: NavigateFunction;
|
|
14
12
|
};
|
|
15
13
|
|
|
16
|
-
export const createHeader = ({
|
|
14
|
+
export const createHeader = ({ navigate }: CreateHeaderOpts) => {
|
|
17
15
|
return observer(() => {
|
|
18
16
|
return (
|
|
19
17
|
<Header
|
|
20
18
|
onBack={() => {
|
|
21
|
-
|
|
19
|
+
navigate("/todos");
|
|
22
20
|
}}
|
|
23
21
|
/>
|
|
24
22
|
);
|
package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/single_todo_controller.ts
RENAMED
|
@@ -6,7 +6,7 @@ import { action } from "mobx";
|
|
|
6
6
|
|
|
7
7
|
import { api } from "@/api/_generated/api";
|
|
8
8
|
import type { TodoId } from "@/api/todos/types";
|
|
9
|
-
import { convex } from "@/web/
|
|
9
|
+
import { convex } from "@/web/libs/convex_query_client";
|
|
10
10
|
|
|
11
11
|
export class SingleTodoController {
|
|
12
12
|
@action
|
package/templates/biome.json
CHANGED