pjdev2d-cli 1.1.3 → 1.2.1

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 CHANGED
@@ -9,3 +9,9 @@ npx pjdev2d-cli add <name>
9
9
  ```bash
10
10
  npx pjdev2d-cli add tanstack-query-template
11
11
  ```
12
+
13
+ # To add React Router Template
14
+
15
+ ```bash
16
+ npx pjdev2d-cli add react-router-template
17
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pjdev2d-cli",
3
- "version": "1.1.3",
3
+ "version": "1.2.1",
4
4
  "type": "module",
5
5
  "description": "CLI to install reusable React components like shadcn/ui",
6
6
  "main": "bin/cli.js",
@@ -30,6 +30,7 @@
30
30
  "fs-extra": "^11.3.5",
31
31
  "inquirer": "^13.4.3",
32
32
  "react": "^19.2.6",
33
+ "react-router-dom": "^7.16.0",
33
34
  "tailwind-merge": "^3.6.0"
34
35
  }
35
36
  }
package/registry.json CHANGED
@@ -8,5 +8,10 @@
8
8
  "name": "Tanstack Query",
9
9
  "category": "tanstack-query",
10
10
  "files": ["templates/tanstack-query"]
11
+ },
12
+ "react-router-template": {
13
+ "name": "React Router",
14
+ "category": "react-router",
15
+ "files": ["templates/react-router"]
11
16
  }
12
17
  }
@@ -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,5 @@
1
+ import { Outlet } from "react-router-dom";
2
+
3
+ export function AuthLayout() {
4
+ return <Outlet />;
5
+ }
@@ -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,5 @@
1
+ import { Outlet } from "react-router-dom";
2
+
3
+ export function MainLayout() {
4
+ return <Outlet />;
5
+ }
@@ -0,0 +1,7 @@
1
+ export const PATH = {
2
+ page_root: () => "/" as const,
3
+ page_one: () => "/page_one" as const,
4
+ page_two: () => "/page_two" as const,
5
+ page_auth: () => "/page_auth" as const,
6
+ not_found: () => "*" as const,
7
+ };
@@ -0,0 +1,3 @@
1
+ export default function NotFound() {
2
+ return <div>Not Found</div>;
3
+ }
@@ -0,0 +1,3 @@
1
+ export default function PageAuth() {
2
+ return <div>Page Auth</div>;
3
+ }
@@ -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
+ */
@@ -0,0 +1,5 @@
1
+ export const PageOneDefaultSearchParam = {
2
+ param_one: "",
3
+ param_two: "",
4
+ param_three: "",
5
+ };
@@ -0,0 +1,3 @@
1
+ export default function PageRoot() {
2
+ return <div>Page Root</div>;
3
+ }
@@ -0,0 +1,3 @@
1
+ export default function PageTwo() {
2
+ return <div>Page Two</div>;
3
+ }
@@ -0,0 +1,5 @@
1
+ export const PageTwoDefaultSearchParam = {
2
+ param_one: "",
3
+ param_two: "",
4
+ param_three: "",
5
+ };