pejay-ui 1.1.0 → 1.2.0
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 +5 -0
- package/package.json +3 -2
- package/registry.json +6 -0
- package/templates/scaffolds/react-router/hook/useRouterSearch.ts +131 -0
- package/templates/scaffolds/react-router/router/guards/private.route.tsx +56 -0
- package/templates/scaffolds/react-router/router/guards/public.route.tsx +21 -0
- package/templates/scaffolds/react-router/router/index.ts +186 -0
- package/templates/scaffolds/react-router/router/layouts/auth.layout.tsx +5 -0
- package/templates/scaffolds/react-router/router/layouts/error.layout.tsx +15 -0
- package/templates/scaffolds/react-router/router/layouts/main.layout.tsx +5 -0
- package/templates/scaffolds/react-router/router/path.ts +7 -0
- package/templates/scaffolds/react-router/routes/not-found/index.tsx +3 -0
- package/templates/scaffolds/react-router/routes/page-auth/index.tsx +3 -0
- package/templates/scaffolds/react-router/routes/page-one/index.tsx +85 -0
- package/templates/scaffolds/react-router/routes/page-one/search.page-one.ts +5 -0
- package/templates/scaffolds/react-router/routes/page-root/index.tsx +3 -0
- package/templates/scaffolds/react-router/routes/page-two/index.tsx +3 -0
- package/templates/scaffolds/react-router/routes/page-two/search.page-two.ts +5 -0
- package/templates/scaffolds/tanstack-query/api-queries.ts +19 -0
- package/templates/scaffolds/tanstack-query/module/queries.ts +31 -8
package/README.md
CHANGED
|
@@ -136,3 +136,8 @@ Below is the list of components you can add. Each has a copyable command block w
|
|
|
136
136
|
```bash
|
|
137
137
|
npx pejay-ui add tanstack-query-client
|
|
138
138
|
```
|
|
139
|
+
* **`react-router-client`**: Bare-bone React Router client layout, routing structure, and route guard setup.
|
|
140
|
+
```bash
|
|
141
|
+
npx pejay-ui add react-router-client
|
|
142
|
+
```
|
|
143
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pejay-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "react ui components",
|
|
6
6
|
"bin": {
|
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@babel/core": "^7.24.0",
|
|
24
24
|
"@babel/preset-typescript": "^7.24.0",
|
|
25
|
-
"@tanstack/react-query": "^5.100.14",
|
|
26
25
|
"commander": "^12.0.0",
|
|
27
26
|
"fs-extra": "^11.0.0",
|
|
28
27
|
"inquirer": "^9.0.0"
|
|
@@ -39,6 +38,7 @@
|
|
|
39
38
|
"homepage": "https://github.com/pejaybro/pejay-ui#readme",
|
|
40
39
|
"devDependencies": {
|
|
41
40
|
"@floating-ui/react": "^0.27.19",
|
|
41
|
+
"@tanstack/react-query": "^5.101.0",
|
|
42
42
|
"@types/react": "^19.2.15",
|
|
43
43
|
"@types/react-dom": "^19.2.3",
|
|
44
44
|
"clsx": "^2.1.1",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"lucide-react": "^1.17.0",
|
|
47
47
|
"react": "^19.2.6",
|
|
48
48
|
"react-dom": "^19.2.6",
|
|
49
|
+
"react-router-dom": "^7.16.0",
|
|
49
50
|
"tailwind-merge": "^3.6.0",
|
|
50
51
|
"tailwindcss": "^4.3.0",
|
|
51
52
|
"typescript": "^6.0.3"
|
package/registry.json
CHANGED
|
@@ -194,5 +194,11 @@
|
|
|
194
194
|
"category": "tanstack-query",
|
|
195
195
|
"files": ["templates/scaffolds/tanstack-query"],
|
|
196
196
|
"peerDependencies": ["@tanstack/react-query"]
|
|
197
|
+
},
|
|
198
|
+
"react-router-client": {
|
|
199
|
+
"name": "ReactRouterClient",
|
|
200
|
+
"category": "react-router",
|
|
201
|
+
"files": ["templates/scaffolds/react-router"],
|
|
202
|
+
"peerDependencies": ["react-router-dom"]
|
|
197
203
|
}
|
|
198
204
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef } from "react";
|
|
2
|
+
import { useSearchParams } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
export function useRouterSearch<T extends Record<string, any>>({
|
|
5
|
+
defaultParams,
|
|
6
|
+
}: {
|
|
7
|
+
defaultParams: T;
|
|
8
|
+
}) {
|
|
9
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
10
|
+
|
|
11
|
+
const defaultRef = useRef(defaultParams);
|
|
12
|
+
defaultRef.current = defaultParams;
|
|
13
|
+
|
|
14
|
+
const search = useMemo(() => {
|
|
15
|
+
const result = { ...defaultRef.current };
|
|
16
|
+
const keys = Object.keys(defaultRef.current);
|
|
17
|
+
|
|
18
|
+
for (const key of keys) {
|
|
19
|
+
const fallback = defaultRef.current[key];
|
|
20
|
+
if (Array.isArray(fallback)) {
|
|
21
|
+
const val = searchParams.getAll(key);
|
|
22
|
+
if (val.length > 0) {
|
|
23
|
+
result[key as keyof T] = parseValue(val, fallback) as any;
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
const val = searchParams.get(key);
|
|
27
|
+
if (val !== null) {
|
|
28
|
+
result[key as keyof T] = parseValue(val, fallback) as any;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}, [searchParams]);
|
|
34
|
+
|
|
35
|
+
const setSearch = useCallback(
|
|
36
|
+
(
|
|
37
|
+
updates: Partial<Record<keyof T, any>>,
|
|
38
|
+
options?: { resetPage?: boolean; replace?: boolean }
|
|
39
|
+
) => {
|
|
40
|
+
const next = new URLSearchParams(searchParams);
|
|
41
|
+
const keys = Object.keys(defaultRef.current);
|
|
42
|
+
|
|
43
|
+
for (const key of keys) {
|
|
44
|
+
const value = updates[key as keyof T];
|
|
45
|
+
if (value !== undefined) {
|
|
46
|
+
if (value === null || areEqual(value, defaultRef.current[key])) {
|
|
47
|
+
next.delete(key);
|
|
48
|
+
} else if (Array.isArray(value)) {
|
|
49
|
+
next.delete(key);
|
|
50
|
+
value.forEach((val) => {
|
|
51
|
+
if (val !== undefined && val !== null && val !== "") {
|
|
52
|
+
next.append(key, String(val));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
next.set(key, String(value));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Reset pagination
|
|
62
|
+
* when filters/search changes
|
|
63
|
+
*/
|
|
64
|
+
if (options?.resetPage && "page" in defaultRef.current) {
|
|
65
|
+
next.delete("page");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
setSearchParams(next, {
|
|
69
|
+
replace: options?.replace,
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
[searchParams, setSearchParams]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return { search, setSearch };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function areEqual(a: any, b: any) {
|
|
79
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
80
|
+
if (a.length !== b.length) return false;
|
|
81
|
+
const sortedA = [...a].sort();
|
|
82
|
+
const sortedB = [...b].sort();
|
|
83
|
+
return sortedA.every((val, index) => val === sortedB[index]);
|
|
84
|
+
}
|
|
85
|
+
return a === b;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseValue(value: string | string[], fallback: any): any {
|
|
89
|
+
if (Array.isArray(fallback)) {
|
|
90
|
+
const fallbackVal = fallback[0];
|
|
91
|
+
const valArray = Array.isArray(value) ? value : [value];
|
|
92
|
+
return valArray.map((item) => parseSingleValue(item, fallbackVal));
|
|
93
|
+
}
|
|
94
|
+
const singleVal = Array.isArray(value) ? value[0] : value;
|
|
95
|
+
return parseSingleValue(singleVal, fallback);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseSingleValue(value: string, fallback: any) {
|
|
99
|
+
if (typeof fallback === "number") {
|
|
100
|
+
const parsed = Number(value);
|
|
101
|
+
return Number.isNaN(parsed) ? fallback : parsed;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Boolean parsing
|
|
106
|
+
*/
|
|
107
|
+
if (typeof fallback === "boolean") {
|
|
108
|
+
return value === "true";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* String fallback
|
|
113
|
+
*/
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/*
|
|
118
|
+
# NOTE: Array parameter usage example
|
|
119
|
+
|
|
120
|
+
1. Define the default parameter as an array:
|
|
121
|
+
export const DefaultSearch = {
|
|
122
|
+
color: [] as string[],
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
2. Set values (e.g., in a component):
|
|
126
|
+
setSearch({ color: ["red", "blue"] });
|
|
127
|
+
// Resulting URL query: ?color=red&color=blue
|
|
128
|
+
|
|
129
|
+
3. Read values (e.g., in a component):
|
|
130
|
+
const colors = search.color; // Returns ["red", "blue"]
|
|
131
|
+
*/
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Outlet } from "react-router-dom";
|
|
2
|
+
|
|
3
|
+
export function PrivateRoute() {
|
|
4
|
+
return <Outlet />;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
# NOTE : use this when you want to redirect user to login page if he is already logged out.
|
|
9
|
+
# Includes the premium "Redirect Back" pattern to preserve user navigation state.
|
|
10
|
+
|
|
11
|
+
import { Navigate, Outlet, useLocation } from "react-router-dom";
|
|
12
|
+
|
|
13
|
+
export default function PrivateRoute() {
|
|
14
|
+
const token = getLocalAuthToken();
|
|
15
|
+
const location = useLocation();
|
|
16
|
+
|
|
17
|
+
if (!token) {
|
|
18
|
+
// Passes the requested path inside search params so you can redirect back after successful login
|
|
19
|
+
return (
|
|
20
|
+
<Navigate
|
|
21
|
+
to={`/page_auth?redirectTo=${encodeURIComponent(location.pathname + location.search)}`}
|
|
22
|
+
replace
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return <Outlet />;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# NOTE: HOW THE LOGIN COMPONENT CONSUMES THE "redirectTo" PARAMETER
|
|
30
|
+
|
|
31
|
+
### The Bookmarking Lifecycle Flow:
|
|
32
|
+
1. **Bookmarked Access:** A logged-out user tries to access a bookmarked private route directly:
|
|
33
|
+
`https://your-app.com/page_two`
|
|
34
|
+
2. **Redirect to Auth:** The `PrivateRoute` guard intercepts the request, captures the target pathname (`/page_two`), and redirects them to the login screen with the target appended:
|
|
35
|
+
`https://your-app.com/page_auth?redirectTo=%2Fpage_two`
|
|
36
|
+
3. **Login:** The user submits their login credentials.
|
|
37
|
+
4. **Target Restoration:** Upon successful authentication, the Login page checks the URL for `redirectTo` and navigates the user straight to `/page_two` instead of the generic homepage `/`.
|
|
38
|
+
|
|
39
|
+
### Code Implementation Example:
|
|
40
|
+
```typescript
|
|
41
|
+
import { useNavigate, useSearchParams } from "react-router-dom";
|
|
42
|
+
|
|
43
|
+
export default function LoginPage() {
|
|
44
|
+
const navigate = useNavigate();
|
|
45
|
+
const [searchParams] = useSearchParams();
|
|
46
|
+
|
|
47
|
+
const handleLoginSuccess = () => {
|
|
48
|
+
// 1. Read the redirectTo value (e.g., "/page_two") or default to "/"
|
|
49
|
+
const destination = searchParams.get("redirectTo") || "/";
|
|
50
|
+
|
|
51
|
+
// 2. Redirect the user back to their bookmarked page instead of the root page!
|
|
52
|
+
navigate(destination, { replace: true });
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
*/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Outlet } from "react-router-dom";
|
|
2
|
+
|
|
3
|
+
export function PublicRoute() {
|
|
4
|
+
return <Outlet />;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
|
|
9
|
+
# NOTE : use this when you want to redirect user to main page if he is already logged in
|
|
10
|
+
|
|
11
|
+
import { Navigate, Outlet } from "react-router-dom";
|
|
12
|
+
|
|
13
|
+
export default function PublicRoute() {
|
|
14
|
+
const token = getLocalAuthToken();
|
|
15
|
+
if (token) {
|
|
16
|
+
return <Navigate to={PATH.desired_path_name} replace />;
|
|
17
|
+
}
|
|
18
|
+
return <Outlet />;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
*/
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { createBrowserRouter, RouteObject } from "react-router-dom";
|
|
2
|
+
import { PATH } from "./path";
|
|
3
|
+
import { PublicRoute } from "./guards/public.route";
|
|
4
|
+
import { PrivateRoute } from "./guards/private.route";
|
|
5
|
+
import { AuthLayout } from "./layouts/auth.layout";
|
|
6
|
+
import { ErrorLayout } from "./layouts/error.layout";
|
|
7
|
+
import { MainLayout } from "./layouts/main.layout";
|
|
8
|
+
|
|
9
|
+
const PublicRoutes: RouteObject[] = [
|
|
10
|
+
{
|
|
11
|
+
Component: PublicRoute,
|
|
12
|
+
children: [
|
|
13
|
+
{
|
|
14
|
+
Component: AuthLayout,
|
|
15
|
+
ErrorBoundary: ErrorLayout,
|
|
16
|
+
children: [
|
|
17
|
+
{
|
|
18
|
+
path: PATH.page_auth(),
|
|
19
|
+
lazy: async () => {
|
|
20
|
+
const module = await import("../routes/page-auth");
|
|
21
|
+
return { Component: module.default };
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const PrivateRoutes: RouteObject[] = [
|
|
31
|
+
{
|
|
32
|
+
Component: PrivateRoute,
|
|
33
|
+
children: [
|
|
34
|
+
{
|
|
35
|
+
Component: MainLayout,
|
|
36
|
+
ErrorBoundary: ErrorLayout,
|
|
37
|
+
children: [
|
|
38
|
+
{
|
|
39
|
+
path: PATH.page_root(),
|
|
40
|
+
ErrorBoundary: ErrorLayout,
|
|
41
|
+
lazy: async () => {
|
|
42
|
+
const module = await import("../routes/page-root");
|
|
43
|
+
return { Component: module.default };
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
path: PATH.page_one(),
|
|
48
|
+
ErrorBoundary: ErrorLayout,
|
|
49
|
+
lazy: async () => {
|
|
50
|
+
const module = await import("../routes/page-one");
|
|
51
|
+
return { Component: module.default };
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
path: PATH.page_two(),
|
|
56
|
+
ErrorBoundary: ErrorLayout,
|
|
57
|
+
lazy: async () => {
|
|
58
|
+
const module = await import("../routes/page-two");
|
|
59
|
+
return { Component: module.default };
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const NotFoundRoutes: RouteObject[] = [
|
|
69
|
+
{
|
|
70
|
+
path: PATH.not_found(), // Wildcard catch-all must be at the end of the root array
|
|
71
|
+
lazy: async () => {
|
|
72
|
+
const module = await import("../routes/not-found");
|
|
73
|
+
return { Component: module.default };
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
export const router = createBrowserRouter([
|
|
79
|
+
...PublicRoutes,
|
|
80
|
+
...PrivateRoutes,
|
|
81
|
+
...NotFoundRoutes,
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
/*
|
|
85
|
+
|
|
86
|
+
currently i am using new way of using react-router version like
|
|
87
|
+
Component: PrivateLayout,
|
|
88
|
+
lazy: async () => ...
|
|
89
|
+
|
|
90
|
+
the old way is
|
|
91
|
+
|
|
92
|
+
{
|
|
93
|
+
path: "/",
|
|
94
|
+
element: (
|
|
95
|
+
<PublicRoute>
|
|
96
|
+
<AuthLayout />
|
|
97
|
+
</PublicRoute>
|
|
98
|
+
),
|
|
99
|
+
children: [
|
|
100
|
+
{
|
|
101
|
+
path: PATHS.LOGIN,
|
|
102
|
+
element: <Login />,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
| Feature | `errorElement` | `ErrorBoundary` |
|
|
110
|
+
| -------------------- | --------------------------------- | -------------------------- |
|
|
111
|
+
| What you pass | React Element | Component Function |
|
|
112
|
+
| Syntax | `errorElement: <ErrorPage />` | `ErrorBoundary: ErrorPage` |
|
|
113
|
+
| Requires JSX | Yes | No |
|
|
114
|
+
| Works in `.ts` file | No (unless `React.createElement`) | Yes |
|
|
115
|
+
| React Router version | v6.4+ | Newer preferred API |
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
# ANCHOR : 2 ways to show the error boundry page data
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# NOTE : WAY-1 inside layout i.e inplace of outlet
|
|
124
|
+
{
|
|
125
|
+
path: PATH.page_two(),
|
|
126
|
+
ErrorBoundary: ErrorLayout,
|
|
127
|
+
lazy: async () => {
|
|
128
|
+
const module = await import("../routes/page-two");
|
|
129
|
+
return { Component: module.default };
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
# NOTE : WAY-2 replace entire layout with error boundary
|
|
136
|
+
{
|
|
137
|
+
Component: AuthLayout,
|
|
138
|
+
ErrorBoundary: ErrorLayout,
|
|
139
|
+
children: [
|
|
140
|
+
{
|
|
141
|
+
path: PATH.page_auth(),
|
|
142
|
+
lazy: async () => {
|
|
143
|
+
const module = await import("../routes/page-auth");
|
|
144
|
+
return { Component: module.default };
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
# NOTE : DYNAMIC PAGE TITLES (SEO BEST PRACTICE)
|
|
153
|
+
To set browser tab titles dynamically on page transitions, create a custom hook and call it inside your page route components:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { useEffect } from "react";
|
|
157
|
+
|
|
158
|
+
export function useDocumentTitle(title: string) {
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
const original = document.title;
|
|
161
|
+
document.title = `${title} | My App`;
|
|
162
|
+
return () => {
|
|
163
|
+
document.title = original;
|
|
164
|
+
};
|
|
165
|
+
}, [title]);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// In routes/page-one.tsx:
|
|
169
|
+
// useDocumentTitle("Analytics Page");
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
# NOTE : ROUTE-LEVEL SUSPENSE VS. COMPONENT-LEVEL SKELETONS
|
|
175
|
+
|
|
176
|
+
1. **Why we place `<Suspense>` at the very top (wrapping `<RouterProvider>`):**
|
|
177
|
+
- **Code Chunk Loading:** Because we lazy-load routes (`lazy: async () => ...`), React Router has to download the page's `.js` chunk file from the server when navigating.
|
|
178
|
+
- **Preventing UI Freezes:** If you click a link and do not have a top-level Suspense boundary, the app will freeze or flicker on the old page until the file download finishes.
|
|
179
|
+
- **Best Practice:** Wrap the router in `<Suspense fallback={<TopBarProgressBar />} />` so the user gets instant progress feedback when clicking links.
|
|
180
|
+
|
|
181
|
+
2. **Why we use Component-level skeletons:**
|
|
182
|
+
- **Data Loading:** Once the page code is downloaded and mounted, your API data fetching starts.
|
|
183
|
+
- **Interactive UI:** This is where you render custom skeleton loaders inside your page components while waiting for TanStack Query data, offering a premium and localized loading layout.
|
|
184
|
+
*/
|
|
185
|
+
|
|
186
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Outlet, useRouteError } from "react-router-dom";
|
|
2
|
+
|
|
3
|
+
export function ErrorLayout() {
|
|
4
|
+
const error = useRouteError();
|
|
5
|
+
return (
|
|
6
|
+
<div>
|
|
7
|
+
<h1>Something went wrong!</h1>
|
|
8
|
+
<p>
|
|
9
|
+
{error instanceof Error
|
|
10
|
+
? error.message
|
|
11
|
+
: "An unexpected error occurred"}
|
|
12
|
+
</p>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export default function PageOne() {
|
|
2
|
+
return <div>Page One</div>;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
# NOTE: FRAMEWORK-LEVEL META & PRERENDERING CONFIGURATION (SEO)
|
|
7
|
+
|
|
8
|
+
If you are using React Router v7 and want static HTML files compiled for SEO search crawlers, follow these steps:
|
|
9
|
+
|
|
10
|
+
-------------------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
1. ROUTE META EXPORT (Add this inside this page file):
|
|
13
|
+
```typescript
|
|
14
|
+
import type { MetaFunction } from "react-router-dom";
|
|
15
|
+
|
|
16
|
+
export const meta: MetaFunction = () => {
|
|
17
|
+
return [
|
|
18
|
+
{ title: "Page One Analytics | My App" },
|
|
19
|
+
{ name: "description", content: "Analyze product sales and user logs." },
|
|
20
|
+
];
|
|
21
|
+
};
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
-------------------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
2. PRERENDER SETTINGS (Add this in your react-router.config.ts / vite.config.ts):
|
|
27
|
+
```typescript
|
|
28
|
+
export default {
|
|
29
|
+
async prerender() {
|
|
30
|
+
return ["/", "/page_one", "/page_two"]; // Routes to generate as static HTML pages
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
-------------------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
3. HOW THE BUILD CHANGES (Build output comparison):
|
|
38
|
+
|
|
39
|
+
OLD BUILD (Standard Single Page Application):
|
|
40
|
+
- Outputs only a single empty "index.html" page.
|
|
41
|
+
- Crawlers see no initial page HTML or titles when visiting "/page_one" until JS loads.
|
|
42
|
+
|
|
43
|
+
NEW BUILD (Prerendered SPA):
|
|
44
|
+
- Outputs directory folders containing index.html files:
|
|
45
|
+
dist/
|
|
46
|
+
├── index.html <- for "/"
|
|
47
|
+
├── page_one/
|
|
48
|
+
│ └── index.html <- for "/page_one" (contains pre-rendered metadata)
|
|
49
|
+
└── page_two/
|
|
50
|
+
└── index.html <- for "/page_two" (contains pre-rendered metadata)
|
|
51
|
+
|
|
52
|
+
-------------------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
4. SERVER SETTINGS (VPS Deployment):
|
|
55
|
+
|
|
56
|
+
For a standard VPS server (like Nginx), configure it to serve static files from your build directory.
|
|
57
|
+
Nginx will look for the folder-based index files (e.g., /page_one/index.html) first.
|
|
58
|
+
|
|
59
|
+
NGINX VPS CONFIGURATION EXAMPLE (/etc/nginx/sites-available/default):
|
|
60
|
+
```nginx
|
|
61
|
+
server {
|
|
62
|
+
listen 80;
|
|
63
|
+
server_name your-app.com;
|
|
64
|
+
|
|
65
|
+
# Point to your build distribution directory on the VPS
|
|
66
|
+
root /var/www/your-app/dist;
|
|
67
|
+
|
|
68
|
+
index index.html;
|
|
69
|
+
|
|
70
|
+
location / {
|
|
71
|
+
# 1. Checks if exact file exists ($uri)
|
|
72
|
+
# 2. Checks if a directory index.html exists ($uri/) -> Serves pre-rendered SEO pages!
|
|
73
|
+
# 3. Falls back to root index.html if route is handled client-side
|
|
74
|
+
try_files $uri $uri/ /index.html;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Optional: Proxy API requests directly to your Laravel Backend
|
|
78
|
+
location /api/ {
|
|
79
|
+
proxy_pass http://127.0.0.1:8000; # Address where your Laravel backend runs
|
|
80
|
+
proxy_set_header Host $host;
|
|
81
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
*/
|
|
@@ -58,9 +58,28 @@ return <ModuleComponent data={data} />;
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
# NOTE : Wrapping
|
|
61
|
+
<ErrorBoundary fallback={<div>Failed to load Stats</div>}>
|
|
61
62
|
<Suspense fallback={<FallBackComponent />}>
|
|
62
63
|
<ModuleComponent />
|
|
63
64
|
</Suspense>
|
|
65
|
+
</ErrorBoundary>
|
|
66
|
+
|
|
67
|
+
this is comp level error boundary in case of api fails then suspense bubbeling up will touch this first and stops
|
|
68
|
+
in case this is not present then the router level
|
|
69
|
+
so in this way we can only show a particaular comp failed to load not entore page
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
Manually (Option A - useQuery):
|
|
76
|
+
|
|
77
|
+
Code: if (error) return <ErrorComponent />
|
|
78
|
+
Behavior: Bypasses Error Boundaries. Component-level and router-level boundaries never trigger.
|
|
79
|
+
Automatically (Option B - useSuspenseQuery or useQuery with throwOnError: true):
|
|
80
|
+
|
|
81
|
+
Code: Component throws the error during render.
|
|
82
|
+
Behavior: The error bubbles up. It triggers the nearest Error Boundary (either your component-level boundary if you wrapped it, or falls back to the router-level boundary).
|
|
64
83
|
|
|
65
84
|
---------------------------------------------------------------------------------------------------
|
|
66
85
|
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
queryOptions,
|
|
3
|
+
infiniteQueryOptions,
|
|
4
|
+
keepPreviousData,
|
|
5
|
+
} from "@tanstack/react-query";
|
|
2
6
|
|
|
3
7
|
import { ModuleKeys } from "./keys";
|
|
4
8
|
import { ModuleMappers } from "./mappers";
|
|
@@ -15,7 +19,7 @@ export const ModuleQueries = {
|
|
|
15
19
|
*/
|
|
16
20
|
return ModuleService.get_query_name_example();
|
|
17
21
|
},
|
|
18
|
-
select:
|
|
22
|
+
select: raw => {
|
|
19
23
|
/*
|
|
20
24
|
# NOTE: ModuleMappers.fetch_query_name_example() -> manipulates the data from api into desird format before returning to ui or page
|
|
21
25
|
Using the 'select' property memoizes this transformation (only runs when cached data changes).
|
|
@@ -50,7 +54,7 @@ export const ModuleQueries = {
|
|
|
50
54
|
return hasMore ? current_page + 1 : undefined;
|
|
51
55
|
},
|
|
52
56
|
initialPageParam: 1,
|
|
53
|
-
select:
|
|
57
|
+
select: raw => {
|
|
54
58
|
/*
|
|
55
59
|
# NOTE: ModuleMappers.fetch_infinite_query_example() -> manipulates the data from api into desird format before returning to ui or page
|
|
56
60
|
Using the 'select' property memoizes this transformation (only runs when cached data changes).
|
|
@@ -76,7 +80,7 @@ export const ModuleQueries = {
|
|
|
76
80
|
*/
|
|
77
81
|
return ModuleService.get_query_with_params_example(params, signal);
|
|
78
82
|
},
|
|
79
|
-
select:
|
|
83
|
+
select: raw => {
|
|
80
84
|
// Reusing the same mapper example for consistency
|
|
81
85
|
return ModuleMappers.fetch_query_name_example(raw as any);
|
|
82
86
|
},
|
|
@@ -89,7 +93,7 @@ export const ModuleQueries = {
|
|
|
89
93
|
queryFn: ({ signal }) => {
|
|
90
94
|
return ModuleService.get_query_by_id_example(id!, signal);
|
|
91
95
|
},
|
|
92
|
-
select:
|
|
96
|
+
select: raw => {
|
|
93
97
|
return ModuleMappers.fetch_query_name_example(raw as any);
|
|
94
98
|
},
|
|
95
99
|
placeholderData: keepPreviousData,
|
|
@@ -100,13 +104,16 @@ export const ModuleQueries = {
|
|
|
100
104
|
enabled: !!id,
|
|
101
105
|
}),
|
|
102
106
|
|
|
103
|
-
fetch_query_combo_example: (
|
|
107
|
+
fetch_query_combo_example: (
|
|
108
|
+
id: string | null | undefined,
|
|
109
|
+
params: Record<string, any>,
|
|
110
|
+
) =>
|
|
104
111
|
queryOptions({
|
|
105
112
|
queryKey: ModuleKeys.fetch_query_combo_example(id || "", params),
|
|
106
113
|
queryFn: ({ signal }) => {
|
|
107
114
|
return ModuleService.get_query_combo_example(id!, params, signal);
|
|
108
115
|
},
|
|
109
|
-
select:
|
|
116
|
+
select: raw => {
|
|
110
117
|
return ModuleMappers.fetch_query_name_example(raw as any);
|
|
111
118
|
},
|
|
112
119
|
placeholderData: keepPreviousData,
|
|
@@ -129,5 +136,21 @@ export const ModuleQueries = {
|
|
|
129
136
|
- The `select` function is **automatically memoized** by TanStack Query.
|
|
130
137
|
- It will ONLY re-run when the cached raw data changes.
|
|
131
138
|
- If the component re-renders for other reasons (e.g., local UI states, parent re-renders, or window focus checks), TanStack Query skips the mapper execution completely and returns the already memoized mapped data instantly.
|
|
132
|
-
*/
|
|
133
139
|
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
use this inside queries if you want to show error boundary at component level for perticular query when not using useSuspenseQuery or hook which provides suspense by default. by default it just catches the errror and shows the error in useMutation or useQuery hooks but in case of useSuspenseQuery it throws the error and bubbels up to the nearest error boundary.
|
|
151
|
+
|
|
152
|
+
throwOnError: (error) => {
|
|
153
|
+
// Throw to boundary for severe errors (like 404 Not Found or 500 Server Crashes)
|
|
154
|
+
return error.status === 404 || error.status >= 500;
|
|
155
|
+
}
|
|
156
|
+
*/
|