Skip to content

Commit b8224c0

Browse files
authored
feat: migrate to tanstack start (#21)
* feat: migrate to tanstack start * chore: more changes * chore: get auth session * chore: add correct url * chore: correct auth return * chore: hono client works now * chore: add hono with type client * chore: fix theme provider * chore: tanstack start updates * chore: remove deprecated stuff from zod * chore: remove hardcoded urls * chore: remove auth fetching from root route
1 parent 321e9a0 commit b8224c0

File tree

18 files changed

+339
-200
lines changed

18 files changed

+339
-200
lines changed

.vscode/settings.json

Lines changed: 0 additions & 50 deletions
This file was deleted.

apps/client/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929
"@tailwindcss/postcss": "^4.1.16",
3030
"@tailwindcss/vite": "^4.1.16",
3131
"@tanstack/react-query": "^5.90.5",
32-
"@tanstack/react-router": "^1.133.22",
32+
"@tanstack/react-router": "^1.134.13",
33+
"@tanstack/react-router-ssr-query": "^1.134.13",
34+
"@tanstack/react-start": "^1.134.14",
3335
"@tanstack/react-table": "^8.21.3",
3436
"@tiptap/extension-code-block-lowlight": "^2.26.4",
3537
"@tiptap/extension-color": "^2.26.4",
@@ -78,6 +80,7 @@
7880
"tailwindcss": "^4.1.16",
7981
"typescript": "~5.8.3",
8082
"typescript-eslint": "^8.46.2",
81-
"vite": "^6.4.1"
83+
"vite": "^6.4.1",
84+
"vite-tsconfig-paths": "^5.1.4"
8285
}
8386
}

apps/client/src/components/theme-provider.tsx

Lines changed: 71 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1-
import { createContext, useContext, useEffect, useState } from "react";
1+
import { ScriptOnce } from "@tanstack/react-router";
2+
import {
3+
createContext,
4+
use,
5+
useCallback,
6+
useEffect,
7+
useMemo,
8+
useState,
9+
} from "react";
210

311
type Theme = "dark" | "light" | "system";
12+
const MEDIA = "(prefers-color-scheme: dark)";
413

514
type ThemeProviderProps = {
615
children: React.ReactNode;
@@ -20,51 +29,89 @@ const initialState: ThemeProviderState = {
2029

2130
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
2231

32+
// references:
33+
// https://ui.shadcn.com/docs/dark-mode/vite
34+
// https://github.com/pacocoursey/next-themes/blob/main/next-themes/src/index.tsx
2335
export function ThemeProvider({
2436
children,
2537
defaultTheme = "system",
26-
storageKey = "vite-ui-theme",
38+
storageKey = "theme",
2739
...props
2840
}: ThemeProviderProps) {
2941
const [theme, setTheme] = useState<Theme>(
30-
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
42+
() =>
43+
(typeof window !== "undefined"
44+
? (localStorage.getItem(storageKey) as Theme)
45+
: null) || defaultTheme,
3146
);
3247

48+
const handleMediaQuery = useCallback(
49+
(e: MediaQueryListEvent | MediaQueryList) => {
50+
if (theme !== "system") return;
51+
const root = window.document.documentElement;
52+
const targetTheme = e.matches ? "dark" : "light";
53+
if (!root.classList.contains(targetTheme)) {
54+
root.classList.remove("light", "dark");
55+
root.classList.add(targetTheme);
56+
}
57+
},
58+
[theme],
59+
);
60+
61+
// Listen for system preference changes
3362
useEffect(() => {
34-
const root = window.document.documentElement;
63+
const media = window.matchMedia(MEDIA);
3564

36-
root.classList.remove("light", "dark");
65+
media.addEventListener("change", handleMediaQuery);
66+
handleMediaQuery(media);
3767

38-
if (theme === "system") {
39-
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
40-
.matches
41-
? "dark"
42-
: "light";
68+
return () => media.removeEventListener("change", handleMediaQuery);
69+
}, [handleMediaQuery]);
4370

44-
root.classList.add(systemTheme);
45-
return;
46-
}
71+
useEffect(() => {
72+
const root = window.document.documentElement;
4773

48-
root.classList.add(theme);
49-
}, [theme]);
74+
let targetTheme: string;
5075

51-
const value = {
52-
theme,
53-
setTheme: (theme: Theme) => {
76+
if (theme === "system") {
77+
localStorage.removeItem(storageKey);
78+
targetTheme = window.matchMedia(MEDIA).matches ? "dark" : "light";
79+
} else {
5480
localStorage.setItem(storageKey, theme);
55-
setTheme(theme);
56-
},
57-
};
81+
targetTheme = theme;
82+
}
83+
84+
// Only update if the target theme is not already applied
85+
if (!root.classList.contains(targetTheme)) {
86+
root.classList.remove("light", "dark");
87+
root.classList.add(targetTheme);
88+
}
89+
}, [theme, storageKey]);
90+
91+
const value = useMemo(
92+
() => ({
93+
theme,
94+
setTheme,
95+
}),
96+
[theme],
97+
);
5898

5999
return (
60-
<ThemeProviderContext.Provider {...props} value={value}>
100+
<ThemeProviderContext {...props} value={value}>
101+
<ScriptOnce>
102+
{/* Apply theme early to avoid FOUC */}
103+
{`document.documentElement.classList.toggle(
104+
'dark',
105+
localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
106+
)`}
107+
</ScriptOnce>
61108
{children}
62-
</ThemeProviderContext.Provider>
109+
</ThemeProviderContext>
63110
);
64111
}
65112

66113
export const useTheme = () => {
67-
const context = useContext(ThemeProviderContext);
114+
const context = use(ThemeProviderContext);
68115

69116
if (context === undefined)
70117
throw new Error("useTheme must be used within a ThemeProvider");

apps/client/src/lib/api.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { hc } from "hono/client";
1+
import { hcWithType } from "@server/lib/hc";
22
import type {
3-
AppType,
43
TCreateCheckoutType,
54
TCreateLessonType,
65
TGetAllCreatorCoursesType,
@@ -16,7 +15,15 @@ import {
1615
TUpdateChapterType,
1716
} from "@server/shared/types";
1817

19-
const client = hc<AppType>("/api");
18+
import env from "@server/env";
19+
20+
// include credentials to send cookies to the server
21+
// replace hardcoded url
22+
const client = hcWithType(env.API_URL, {
23+
init: {
24+
credentials: "include",
25+
},
26+
});
2027

2128
export const getAllCourses = async ({
2229
query,
@@ -167,7 +174,7 @@ export const updateCourse = async (id: string, data: TUpdateCourseType) => {
167174

168175
export const createCourseChapter = async (
169176
id: string,
170-
data: TCreateChapterType,
177+
data: TCreateChapterType
171178
) => {
172179
const res = await client.creator.courses[":id"].chapters.$post({
173180
param: {
@@ -189,7 +196,7 @@ export const createCourseChapter = async (
189196
export const createChapterLesson = async (
190197
id: string,
191198
chapterId: string,
192-
data: TCreateLessonType,
199+
data: TCreateLessonType
193200
) => {
194201
const res = await client.creator.courses[":id"].chapters[
195202
":chapterId"
@@ -214,7 +221,7 @@ export const createChapterLesson = async (
214221
export const updateCourseChapter = async (
215222
id: string,
216223
chapterId: string,
217-
data: TUpdateChapterType,
224+
data: TUpdateChapterType
218225
) => {
219226
const res = await client.creator.courses[":id"].chapters[":chapterId"].$patch(
220227
{
@@ -225,7 +232,7 @@ export const updateCourseChapter = async (
225232
json: {
226233
...data,
227234
},
228-
},
235+
}
229236
);
230237

231238
if (!res.ok) {
@@ -258,7 +265,7 @@ export const updateCourseLesson = async (
258265
id: string,
259266
chapterId: string,
260267
lessonId: string,
261-
data: TUpdateLessonType,
268+
data: TUpdateLessonType
262269
) => {
263270
const res = await client.creator.courses[":id"].chapters[
264271
":chapterId"
@@ -284,7 +291,7 @@ export const updateCourseLesson = async (
284291
export const deleteCourseLesson = async (
285292
id: string,
286293
chapterId: string,
287-
lessonId: string,
294+
lessonId: string
288295
) => {
289296
const res = await client.creator.courses[":id"].chapters[
290297
":chapterId"
@@ -444,7 +451,7 @@ export const createCheckout = async (data: TCreateCheckoutType) => {
444451
}
445452

446453
const checkout = await res.json();
447-
454+
448455
return checkout;
449456
};
450457

apps/client/src/lib/auth-client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { createAuthClient } from "better-auth/react";
22
import { inferAdditionalFields } from "better-auth/client/plugins";
3+
import env from "@server/env";
34

45
export const authClient = createAuthClient({
6+
baseURL: env.BETTER_AUTH_URL,
57
plugins: [
68
inferAdditionalFields({
79
user: {
@@ -15,6 +17,7 @@ export const authClient = createAuthClient({
1517

1618
export const {
1719
useSession,
20+
getSession,
1821
signIn,
1922
signUp,
2023
signOut,

apps/client/src/main.tsx

Lines changed: 0 additions & 45 deletions
This file was deleted.

apps/client/src/routeTree.gen.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,3 +489,12 @@ const rootRouteChildren: RootRouteChildren = {
489489
export const routeTree = rootRouteImport
490490
._addFileChildren(rootRouteChildren)
491491
._addFileTypes<FileRouteTypes>()
492+
493+
import type { getRouter } from './router.tsx'
494+
import type { createStart } from '@tanstack/react-start'
495+
declare module '@tanstack/react-start' {
496+
interface Register {
497+
ssr: true
498+
router: Awaited<ReturnType<typeof getRouter>>
499+
}
500+
}

apps/client/src/router.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { QueryClient } from "@tanstack/react-query";
2+
import { createRouter } from "@tanstack/react-router";
3+
import NotFound from "./components/not-found";
4+
import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query";
5+
6+
import { routeTree } from "./routeTree.gen";
7+
import { LoadingSpinner } from "./components/ui/loading-spinner";
8+
9+
export function getRouter() {
10+
const queryClient = new QueryClient();
11+
12+
const router = createRouter({
13+
routeTree,
14+
context: { auth: undefined, queryClient },
15+
defaultPreload: "intent",
16+
defaultPendingComponent: () => {
17+
return <LoadingSpinner fullScreen />;
18+
},
19+
defaultNotFoundComponent: () => {
20+
return <NotFound />;
21+
},
22+
});
23+
24+
setupRouterSsrQueryIntegration({ router, queryClient });
25+
26+
return router;
27+
}
28+
29+
declare module "@tanstack/react-router" {
30+
interface Register {
31+
router: ReturnType<typeof getRouter>;
32+
}
33+
}

0 commit comments

Comments
 (0)