create-velox-app 0.4.6 → 0.4.9
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/dist/index.js.map +1 -1
- package/dist/templates/compiler.d.ts.map +1 -1
- package/dist/templates/compiler.js.map +1 -1
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +9 -1
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/placeholders.d.ts +9 -0
- package/dist/templates/placeholders.d.ts.map +1 -1
- package/dist/templates/placeholders.js +31 -5
- package/dist/templates/placeholders.js.map +1 -1
- package/dist/templates/shared/web-base.d.ts +4 -2
- package/dist/templates/shared/web-base.d.ts.map +1 -1
- package/dist/templates/shared/web-base.js +15 -6
- package/dist/templates/shared/web-base.js.map +1 -1
- package/dist/templates/trpc.d.ts +15 -0
- package/dist/templates/trpc.d.ts.map +1 -0
- package/dist/templates/trpc.js +89 -0
- package/dist/templates/trpc.js.map +1 -0
- package/dist/templates/types.d.ts +1 -1
- package/dist/templates/types.d.ts.map +1 -1
- package/dist/templates/types.js +6 -0
- package/dist/templates/types.js.map +1 -1
- package/package.json +2 -2
- package/src/templates/source/api/config/auth.ts +7 -0
- package/src/templates/source/api/index.auth.ts +13 -2
- package/src/templates/source/api/index.default.ts +6 -1
- package/src/templates/source/api/index.trpc.ts +64 -0
- package/src/templates/source/api/package.auth.json +1 -0
- package/src/templates/source/api/package.default.json +1 -0
- package/src/templates/source/api/prisma.config.ts +1 -0
- package/src/templates/source/api/procedures/auth.ts +14 -8
- package/src/templates/source/api/procedures/health.ts +1 -1
- package/src/templates/source/api/procedures/users.auth.ts +24 -68
- package/src/templates/source/api/procedures/users.default.ts +28 -58
- package/src/templates/source/api/schemas/user.ts +9 -4
- package/src/templates/source/api/tsconfig.json +3 -2
- package/src/templates/source/web/App.module.css +54 -2
- package/src/templates/source/web/api.ts +42 -0
- package/src/templates/source/web/main.tsx +63 -13
- package/src/templates/source/web/package.json +10 -8
- package/src/templates/source/web/routes/__root.tsx +42 -3
- package/src/templates/source/web/routes/about.tsx +8 -2
- package/src/templates/source/web/routes/index.auth.tsx +25 -71
- package/src/templates/source/web/routes/index.default.tsx +12 -32
- package/src/templates/source/web/routes/users.tsx +85 -0
- package/src/templates/source/web/tsconfig.json +2 -1
- package/src/templates/source/web/vite.config.ts +4 -3
|
@@ -1,19 +1,18 @@
|
|
|
1
|
+
import { createRouter, RouterProvider } from '@tanstack/react-router';
|
|
2
|
+
import { VeloxProvider } from '@veloxts/client/react';
|
|
1
3
|
import { StrictMode } from 'react';
|
|
2
4
|
import { createRoot } from 'react-dom/client';
|
|
3
|
-
|
|
4
|
-
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
5
|
+
|
|
5
6
|
import { routeTree } from './routeTree.gen';
|
|
6
7
|
import './styles/global.css';
|
|
7
8
|
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
},
|
|
16
|
-
});
|
|
9
|
+
// Import router type from API for full type safety
|
|
10
|
+
import type { AppRouter } from '../../api/src/index.js';
|
|
11
|
+
/* @if auth */
|
|
12
|
+
// Import routes directly from backend - no manual duplication needed
|
|
13
|
+
import { routes } from '../../api/src/index.js';
|
|
14
|
+
|
|
15
|
+
/* @endif auth */
|
|
17
16
|
|
|
18
17
|
// Create router with route tree
|
|
19
18
|
const router = createRouter({ routeTree });
|
|
@@ -25,14 +24,65 @@ declare module '@tanstack/react-router' {
|
|
|
25
24
|
}
|
|
26
25
|
}
|
|
27
26
|
|
|
27
|
+
/* @if auth */
|
|
28
|
+
// Dynamic headers for auth - fetches token on each request
|
|
29
|
+
const getAuthHeaders = () => {
|
|
30
|
+
const token = localStorage.getItem('token');
|
|
31
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Automatic token refresh on 401 responses
|
|
35
|
+
const handleUnauthorized = async (): Promise<boolean> => {
|
|
36
|
+
const refreshToken = localStorage.getItem('refreshToken');
|
|
37
|
+
if (!refreshToken) return false;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch('/api/auth/refresh', {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'Content-Type': 'application/json' },
|
|
43
|
+
body: JSON.stringify({ refreshToken }),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
// Refresh failed - clear tokens
|
|
48
|
+
localStorage.removeItem('token');
|
|
49
|
+
localStorage.removeItem('refreshToken');
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const data = (await res.json()) as { accessToken: string; refreshToken: string };
|
|
54
|
+
localStorage.setItem('token', data.accessToken);
|
|
55
|
+
localStorage.setItem('refreshToken', data.refreshToken);
|
|
56
|
+
return true; // Retry the original request
|
|
57
|
+
} catch {
|
|
58
|
+
// Network error during refresh
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
/* @endif auth */
|
|
63
|
+
|
|
28
64
|
// Render application
|
|
29
65
|
const rootElement = document.getElementById('root');
|
|
30
66
|
if (!rootElement) throw new Error('Root element not found');
|
|
31
67
|
|
|
32
68
|
createRoot(rootElement).render(
|
|
33
69
|
<StrictMode>
|
|
34
|
-
|
|
70
|
+
{/* @if default */}
|
|
71
|
+
<VeloxProvider<AppRouter> config={{ baseUrl: '/api' }}>
|
|
72
|
+
<RouterProvider router={router} />
|
|
73
|
+
</VeloxProvider>
|
|
74
|
+
{/* @endif default */}
|
|
75
|
+
{/* @if auth */}
|
|
76
|
+
<VeloxProvider<AppRouter>
|
|
77
|
+
config={{
|
|
78
|
+
baseUrl: '/api',
|
|
79
|
+
headers: getAuthHeaders,
|
|
80
|
+
routes,
|
|
81
|
+
onUnauthorized: handleUnauthorized,
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
35
84
|
<RouterProvider router={router} />
|
|
36
|
-
</
|
|
85
|
+
</VeloxProvider>
|
|
86
|
+
{/* @endif auth */}
|
|
37
87
|
</StrictMode>
|
|
38
88
|
);
|
|
@@ -10,17 +10,19 @@
|
|
|
10
10
|
"type-check": "tsc --noEmit"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"react": "
|
|
14
|
-
"react-
|
|
15
|
-
"@
|
|
16
|
-
"
|
|
13
|
+
"@tanstack/react-query": "5.90.12",
|
|
14
|
+
"@tanstack/react-router": "1.140.5",
|
|
15
|
+
"@veloxts/client": "__VELOXTS_VERSION__",
|
|
16
|
+
"react": "19.2.1",
|
|
17
|
+
"react-dom": "19.2.1",
|
|
18
|
+
"react-error-boundary": "5.0.0"
|
|
17
19
|
},
|
|
18
20
|
"devDependencies": {
|
|
19
|
-
"@types/react": "19.
|
|
20
|
-
"@types/react-dom": "19.
|
|
21
|
+
"@types/react": "19.2.7",
|
|
22
|
+
"@types/react-dom": "19.2.3",
|
|
21
23
|
"@vitejs/plugin-react": "5.1.2",
|
|
22
|
-
"@tanstack/router-plugin": "1.140.
|
|
23
|
-
"vite": "
|
|
24
|
+
"@tanstack/router-plugin": "1.140.5",
|
|
25
|
+
"vite": "7.2.7",
|
|
24
26
|
"typescript": "5.9.3"
|
|
25
27
|
}
|
|
26
28
|
}
|
|
@@ -1,24 +1,63 @@
|
|
|
1
|
-
import { createRootRoute,
|
|
1
|
+
import { createRootRoute, Link, Outlet } from '@tanstack/react-router';
|
|
2
|
+
/* @if auth */
|
|
3
|
+
import { useQuery } from '@veloxts/client/react';
|
|
4
|
+
|
|
5
|
+
import type { AppRouter } from '../../../api/src/index.js';
|
|
2
6
|
import styles from '@/App.module.css';
|
|
7
|
+
/* @endif auth */
|
|
3
8
|
|
|
4
9
|
export const Route = createRootRoute({
|
|
5
10
|
component: RootLayout,
|
|
6
11
|
});
|
|
7
12
|
|
|
8
13
|
function RootLayout() {
|
|
14
|
+
/* @if auth */
|
|
15
|
+
const { data: user } = useQuery<AppRouter, 'auth', 'getMe'>(
|
|
16
|
+
'auth',
|
|
17
|
+
'getMe',
|
|
18
|
+
{},
|
|
19
|
+
{ retry: false }
|
|
20
|
+
);
|
|
21
|
+
const isAuthenticated = !!user;
|
|
22
|
+
/* @endif auth */
|
|
23
|
+
|
|
9
24
|
return (
|
|
10
25
|
<div className={styles.app}>
|
|
11
26
|
<nav className={styles.nav}>
|
|
12
27
|
<div className={styles.navBrand}>
|
|
13
28
|
<Link to="/" className={styles.logo}>
|
|
14
|
-
Velox
|
|
29
|
+
Velox TS
|
|
15
30
|
</Link>
|
|
16
31
|
</div>
|
|
17
32
|
<div className={styles.navLinks}>
|
|
18
33
|
<Link to="/" className={styles.navLink} activeProps={{ className: styles.navLinkActive }}>
|
|
19
34
|
Home
|
|
20
35
|
</Link>
|
|
21
|
-
|
|
36
|
+
{/* @if default */}
|
|
37
|
+
<Link
|
|
38
|
+
to="/users"
|
|
39
|
+
className={styles.navLink}
|
|
40
|
+
activeProps={{ className: styles.navLinkActive }}
|
|
41
|
+
>
|
|
42
|
+
Users
|
|
43
|
+
</Link>
|
|
44
|
+
{/* @endif default */}
|
|
45
|
+
{/* @if auth */}
|
|
46
|
+
{isAuthenticated && (
|
|
47
|
+
<Link
|
|
48
|
+
to="/users"
|
|
49
|
+
className={styles.navLink}
|
|
50
|
+
activeProps={{ className: styles.navLinkActive }}
|
|
51
|
+
>
|
|
52
|
+
Users
|
|
53
|
+
</Link>
|
|
54
|
+
)}
|
|
55
|
+
{/* @endif auth */}
|
|
56
|
+
<Link
|
|
57
|
+
to="/about"
|
|
58
|
+
className={styles.navLink}
|
|
59
|
+
activeProps={{ className: styles.navLinkActive }}
|
|
60
|
+
>
|
|
22
61
|
About
|
|
23
62
|
</Link>
|
|
24
63
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createFileRoute } from '@tanstack/react-router';
|
|
2
|
+
|
|
2
3
|
import styles from '@/App.module.css';
|
|
3
4
|
|
|
4
5
|
export const Route = createFileRoute('/about')({
|
|
@@ -18,12 +19,17 @@ function AboutPage() {
|
|
|
18
19
|
<div className={styles.cards}>
|
|
19
20
|
<div className={styles.card}>
|
|
20
21
|
<h2>Type Safety</h2>
|
|
21
|
-
<p>
|
|
22
|
+
<p>
|
|
23
|
+
End-to-end type safety without code generation. Types flow from backend to frontend
|
|
24
|
+
automatically.
|
|
25
|
+
</p>
|
|
22
26
|
</div>
|
|
23
27
|
|
|
24
28
|
<div className={styles.card}>
|
|
25
29
|
<h2>Developer Experience</h2>
|
|
26
|
-
<p>
|
|
30
|
+
<p>
|
|
31
|
+
Convention over configuration. Sensible defaults with escape hatches when you need them.
|
|
32
|
+
</p>
|
|
27
33
|
</div>
|
|
28
34
|
|
|
29
35
|
<div className={styles.card}>
|
|
@@ -1,48 +1,9 @@
|
|
|
1
1
|
import { createFileRoute } from '@tanstack/react-router';
|
|
2
|
-
import {
|
|
2
|
+
import { useQueryClient } from '@veloxts/client/react';
|
|
3
3
|
import { useState } from 'react';
|
|
4
|
-
import styles from '@/App.module.css';
|
|
5
|
-
|
|
6
|
-
// API helpers
|
|
7
|
-
const api = {
|
|
8
|
-
get: async <T,>(path: string): Promise<T> => {
|
|
9
|
-
const token = localStorage.getItem('accessToken');
|
|
10
|
-
const res = await fetch(`/api${path}`, {
|
|
11
|
-
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
12
|
-
});
|
|
13
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
14
|
-
return res.json();
|
|
15
|
-
},
|
|
16
|
-
post: async <T,>(path: string, data: unknown): Promise<T> => {
|
|
17
|
-
const token = localStorage.getItem('accessToken');
|
|
18
|
-
const res = await fetch(`/api${path}`, {
|
|
19
|
-
method: 'POST',
|
|
20
|
-
headers: {
|
|
21
|
-
'Content-Type': 'application/json',
|
|
22
|
-
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
23
|
-
},
|
|
24
|
-
body: JSON.stringify(data),
|
|
25
|
-
});
|
|
26
|
-
if (!res.ok) {
|
|
27
|
-
const error = await res.json().catch(() => ({}));
|
|
28
|
-
throw new Error(error.message || `HTTP ${res.status}`);
|
|
29
|
-
}
|
|
30
|
-
return res.json();
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
interface User {
|
|
35
|
-
id: string;
|
|
36
|
-
name: string;
|
|
37
|
-
email: string;
|
|
38
|
-
roles?: string[];
|
|
39
|
-
}
|
|
40
4
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
accessToken: string;
|
|
44
|
-
refreshToken: string;
|
|
45
|
-
}
|
|
5
|
+
import styles from '@/App.module.css';
|
|
6
|
+
import { api } from '@/api';
|
|
46
7
|
|
|
47
8
|
export const Route = createFileRoute('/')({
|
|
48
9
|
component: HomePage,
|
|
@@ -57,19 +18,13 @@ function HomePage() {
|
|
|
57
18
|
const [error, setError] = useState('');
|
|
58
19
|
|
|
59
20
|
// Check if user is logged in
|
|
60
|
-
const { data: user, isLoading } = useQuery({
|
|
61
|
-
queryKey: ['me'],
|
|
62
|
-
queryFn: () => api.get<User>('/auth/me'),
|
|
63
|
-
retry: false,
|
|
64
|
-
});
|
|
21
|
+
const { data: user, isLoading } = api.auth.getMe.useQuery({}, { retry: false });
|
|
65
22
|
|
|
66
|
-
const login = useMutation({
|
|
67
|
-
mutationFn: (data: { email: string; password: string }) =>
|
|
68
|
-
api.post<AuthResponse>('/auth/login', data),
|
|
23
|
+
const login = api.auth.createSession.useMutation({
|
|
69
24
|
onSuccess: (data) => {
|
|
70
|
-
localStorage.setItem('
|
|
25
|
+
localStorage.setItem('token', data.accessToken);
|
|
71
26
|
localStorage.setItem('refreshToken', data.refreshToken);
|
|
72
|
-
|
|
27
|
+
api.auth.getMe.invalidate(undefined, queryClient);
|
|
73
28
|
setError('');
|
|
74
29
|
},
|
|
75
30
|
onError: (err) => {
|
|
@@ -77,13 +32,11 @@ function HomePage() {
|
|
|
77
32
|
},
|
|
78
33
|
});
|
|
79
34
|
|
|
80
|
-
const register = useMutation({
|
|
81
|
-
mutationFn: (data: { name: string; email: string; password: string }) =>
|
|
82
|
-
api.post<AuthResponse>('/auth/register', data),
|
|
35
|
+
const register = api.auth.createAccount.useMutation({
|
|
83
36
|
onSuccess: (data) => {
|
|
84
|
-
localStorage.setItem('
|
|
37
|
+
localStorage.setItem('token', data.accessToken);
|
|
85
38
|
localStorage.setItem('refreshToken', data.refreshToken);
|
|
86
|
-
|
|
39
|
+
api.auth.getMe.invalidate(undefined, queryClient);
|
|
87
40
|
setError('');
|
|
88
41
|
},
|
|
89
42
|
onError: (err) => {
|
|
@@ -91,12 +44,11 @@ function HomePage() {
|
|
|
91
44
|
},
|
|
92
45
|
});
|
|
93
46
|
|
|
94
|
-
const logout = useMutation({
|
|
95
|
-
mutationFn: () => api.post('/auth/logout', {}),
|
|
47
|
+
const logout = api.auth.deleteSession.useMutation({
|
|
96
48
|
onSuccess: () => {
|
|
97
|
-
localStorage.removeItem('
|
|
49
|
+
localStorage.removeItem('token');
|
|
98
50
|
localStorage.removeItem('refreshToken');
|
|
99
|
-
|
|
51
|
+
api.auth.getMe.setData({}, null, queryClient);
|
|
100
52
|
},
|
|
101
53
|
});
|
|
102
54
|
|
|
@@ -131,14 +83,18 @@ function HomePage() {
|
|
|
131
83
|
<div className={styles.cards}>
|
|
132
84
|
<div className={styles.card}>
|
|
133
85
|
<h2>Your Profile</h2>
|
|
134
|
-
<p
|
|
135
|
-
|
|
86
|
+
<p>
|
|
87
|
+
<strong>ID:</strong> {user.id}
|
|
88
|
+
</p>
|
|
89
|
+
<p>
|
|
90
|
+
<strong>Roles:</strong> {user.roles?.join(', ') || 'user'}
|
|
91
|
+
</p>
|
|
136
92
|
</div>
|
|
137
93
|
|
|
138
94
|
<div className={styles.card}>
|
|
139
95
|
<h2>Actions</h2>
|
|
140
96
|
<button
|
|
141
|
-
onClick={() => logout.mutate()}
|
|
97
|
+
onClick={() => logout.mutate({})}
|
|
142
98
|
className={styles.button}
|
|
143
99
|
disabled={logout.isPending}
|
|
144
100
|
>
|
|
@@ -155,9 +111,7 @@ function HomePage() {
|
|
|
155
111
|
<div className={styles.container}>
|
|
156
112
|
<div className={styles.hero}>
|
|
157
113
|
<h1 className={styles.title}>Welcome to VeloxTS</h1>
|
|
158
|
-
<p className={styles.subtitle}>
|
|
159
|
-
Full-stack TypeScript with authentication.
|
|
160
|
-
</p>
|
|
114
|
+
<p className={styles.subtitle}>Full-stack TypeScript with authentication.</p>
|
|
161
115
|
</div>
|
|
162
116
|
|
|
163
117
|
<div className={styles.authCard}>
|
|
@@ -215,14 +169,14 @@ function HomePage() {
|
|
|
215
169
|
>
|
|
216
170
|
{login.isPending || register.isPending
|
|
217
171
|
? 'Please wait...'
|
|
218
|
-
: isLogin
|
|
172
|
+
: isLogin
|
|
173
|
+
? 'Sign In'
|
|
174
|
+
: 'Create Account'}
|
|
219
175
|
</button>
|
|
220
176
|
</form>
|
|
221
177
|
|
|
222
178
|
{!isLogin && (
|
|
223
|
-
<p className={styles.formHint}>
|
|
224
|
-
Password: 12+ chars, uppercase, lowercase, number
|
|
225
|
-
</p>
|
|
179
|
+
<p className={styles.formHint}>Password: 12+ chars, uppercase, lowercase, number</p>
|
|
226
180
|
)}
|
|
227
181
|
</div>
|
|
228
182
|
</div>
|
|
@@ -1,40 +1,20 @@
|
|
|
1
1
|
import { createFileRoute } from '@tanstack/react-router';
|
|
2
|
-
import { useQuery } from '@tanstack/react-query';
|
|
3
|
-
import styles from '@/App.module.css';
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
get: async <T,>(path: string): Promise<T> => {
|
|
8
|
-
const res = await fetch(`/api${path}`);
|
|
9
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
10
|
-
return res.json();
|
|
11
|
-
},
|
|
12
|
-
};
|
|
3
|
+
import styles from '@/App.module.css';
|
|
4
|
+
import { api } from '@/api';
|
|
13
5
|
|
|
14
6
|
export const Route = createFileRoute('/')({
|
|
15
7
|
component: HomePage,
|
|
16
8
|
});
|
|
17
9
|
|
|
18
|
-
interface HealthResponse {
|
|
19
|
-
status: string;
|
|
20
|
-
version: string;
|
|
21
|
-
timestamp: string;
|
|
22
|
-
uptime: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
10
|
function HomePage() {
|
|
26
|
-
const { data: health, isLoading, error } = useQuery({
|
|
27
|
-
queryKey: ['health'],
|
|
28
|
-
queryFn: () => api.get<HealthResponse>('/health'),
|
|
29
|
-
});
|
|
11
|
+
const { data: health, isLoading, error } = api.health.check.useQuery({});
|
|
30
12
|
|
|
31
13
|
return (
|
|
32
14
|
<div className={styles.container}>
|
|
33
15
|
<div className={styles.hero}>
|
|
34
16
|
<h1 className={styles.title}>Welcome to VeloxTS</h1>
|
|
35
|
-
<p className={styles.subtitle}>
|
|
36
|
-
Full-stack TypeScript, beautifully simple.
|
|
37
|
-
</p>
|
|
17
|
+
<p className={styles.subtitle}>Full-stack TypeScript, beautifully simple.</p>
|
|
38
18
|
</div>
|
|
39
19
|
|
|
40
20
|
<div className={styles.cards}>
|
|
@@ -45,19 +25,19 @@ function HomePage() {
|
|
|
45
25
|
) : error ? (
|
|
46
26
|
<p className={styles.error}>Disconnected</p>
|
|
47
27
|
) : (
|
|
48
|
-
<p className={styles.success}>
|
|
49
|
-
{health?.status === 'ok' ? 'Connected' : 'Unknown'}
|
|
50
|
-
</p>
|
|
51
|
-
)}
|
|
52
|
-
{health && (
|
|
53
|
-
<p className={styles.meta}>v{health.version}</p>
|
|
28
|
+
<p className={styles.success}>{health?.status === 'ok' ? 'Connected' : 'Unknown'}</p>
|
|
54
29
|
)}
|
|
30
|
+
{health && <p className={styles.meta}>v{health.version}</p>}
|
|
55
31
|
</div>
|
|
56
32
|
|
|
57
33
|
<div className={styles.card}>
|
|
58
34
|
<h2>Get Started</h2>
|
|
59
|
-
<p>
|
|
60
|
-
|
|
35
|
+
<p>
|
|
36
|
+
Edit <code>apps/api/src/procedures</code> to add API endpoints.
|
|
37
|
+
</p>
|
|
38
|
+
<p>
|
|
39
|
+
Edit <code>apps/web/src/routes</code> to add pages.
|
|
40
|
+
</p>
|
|
61
41
|
</div>
|
|
62
42
|
|
|
63
43
|
<div className={styles.card}>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Users Page - Demonstrates React 19 patterns with VeloxTS
|
|
3
|
+
*
|
|
4
|
+
* Uses useSuspenseQuery with Suspense boundaries for cleaner component code.
|
|
5
|
+
* Data is guaranteed to be available when the component renders.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
9
|
+
import { Suspense } from 'react';
|
|
10
|
+
import { ErrorBoundary } from 'react-error-boundary';
|
|
11
|
+
|
|
12
|
+
import styles from '@/App.module.css';
|
|
13
|
+
import { api } from '@/api';
|
|
14
|
+
|
|
15
|
+
export const Route = createFileRoute('/users')({
|
|
16
|
+
component: UsersPage,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function UsersPage() {
|
|
20
|
+
return (
|
|
21
|
+
<div className={styles.container}>
|
|
22
|
+
<div className={styles.hero}>
|
|
23
|
+
<h1 className={styles.title}>Users</h1>
|
|
24
|
+
<p className={styles.subtitle}>Type-safe data fetching with React 19 Suspense</p>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<ErrorBoundary fallback={<UsersError />}>
|
|
28
|
+
<Suspense fallback={<UsersLoading />}>
|
|
29
|
+
<UsersTable />
|
|
30
|
+
</Suspense>
|
|
31
|
+
</ErrorBoundary>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Users table component - wrapped in Suspense
|
|
38
|
+
*
|
|
39
|
+
* With useSuspenseQuery, data is guaranteed to be available.
|
|
40
|
+
* No need for isLoading/error checks - handled by boundaries.
|
|
41
|
+
*/
|
|
42
|
+
function UsersTable() {
|
|
43
|
+
const { data } = api.users.listUsers.useSuspenseQuery({});
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className={styles.tableContainer}>
|
|
47
|
+
<table className={styles.table}>
|
|
48
|
+
<thead>
|
|
49
|
+
<tr>
|
|
50
|
+
<th>Name</th>
|
|
51
|
+
<th>Email</th>
|
|
52
|
+
<th>Created</th>
|
|
53
|
+
</tr>
|
|
54
|
+
</thead>
|
|
55
|
+
<tbody>
|
|
56
|
+
{data.data.map((user) => (
|
|
57
|
+
<tr key={user.id}>
|
|
58
|
+
<td>{user.name}</td>
|
|
59
|
+
<td>{user.email}</td>
|
|
60
|
+
<td>{new Date(user.createdAt).toLocaleDateString()}</td>
|
|
61
|
+
</tr>
|
|
62
|
+
))}
|
|
63
|
+
{data.data.length === 0 && (
|
|
64
|
+
<tr>
|
|
65
|
+
<td colSpan={3} className={styles.emptyState}>
|
|
66
|
+
No users found. Create one via the API!
|
|
67
|
+
</td>
|
|
68
|
+
</tr>
|
|
69
|
+
)}
|
|
70
|
+
</tbody>
|
|
71
|
+
</table>
|
|
72
|
+
<p className={styles.meta}>
|
|
73
|
+
Page {data.meta.page} - {data.meta.total} total users
|
|
74
|
+
</p>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function UsersLoading() {
|
|
80
|
+
return <p className={styles.loading}>Loading users...</p>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function UsersError() {
|
|
84
|
+
return <p className={styles.error}>Failed to load users. Please try again.</p>;
|
|
85
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { defineConfig } from 'vite';
|
|
2
|
-
import react from '@vitejs/plugin-react';
|
|
3
|
-
import { tanstackRouter } from '@tanstack/router-plugin/vite';
|
|
4
1
|
import path from 'node:path';
|
|
5
2
|
|
|
3
|
+
import { tanstackRouter } from '@tanstack/router-plugin/vite';
|
|
4
|
+
import react from '@vitejs/plugin-react';
|
|
5
|
+
import { defineConfig } from 'vite';
|
|
6
|
+
|
|
6
7
|
export default defineConfig({
|
|
7
8
|
plugins: [tanstackRouter(), react()],
|
|
8
9
|
resolve: {
|