pejay-ui 1.2.2 → 1.3.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.
Files changed (60) hide show
  1. package/package.json +1 -1
  2. package/registry.json +33 -1
  3. package/templates/notes/create-pejay.md +222 -0
  4. package/templates/notes/notes-v1.md +516 -0
  5. package/templates/notes/notes-v2.md +764 -0
  6. package/templates/notes/notes-v3.md +574 -0
  7. package/templates/notes/notes-v4.md +811 -0
  8. package/templates/notes/notes-v5.md +579 -0
  9. package/templates/notes/notes-vf+1.md +311 -0
  10. package/templates/notes/notes-vfinal.md +852 -0
  11. package/templates/scaffolds/axios/api/index.ts +40 -0
  12. package/templates/scaffolds/axios/api/one.api.ts +94 -0
  13. package/templates/scaffolds/axios/endpoints.ts +9 -0
  14. package/templates/scaffolds/axios/index.ts +26 -0
  15. package/templates/scaffolds/axios/interceptors.ts +103 -0
  16. package/templates/scaffolds/axios/request.ts +32 -0
  17. package/templates/scaffolds/react-router/hook/useRouterSearch.ts +8 -0
  18. package/templates/scaffolds/react-router/router/guards/private.route.tsx +1 -0
  19. package/templates/scaffolds/react-router/router/index.ts +26 -0
  20. package/templates/scaffolds/react-router/router/layouts/error.layout.tsx +1 -1
  21. package/templates/scaffolds/redux-store/middlewares.ts +0 -0
  22. package/templates/scaffolds/redux-store/reducers.ts +30 -0
  23. package/templates/scaffolds/redux-store/selector/one.selector.ts +43 -0
  24. package/templates/scaffolds/redux-store/selector/two.selector.ts +11 -0
  25. package/templates/scaffolds/redux-store/slices/one.slice.ts +202 -0
  26. package/templates/scaffolds/redux-store/slices/two.slice.ts +21 -0
  27. package/templates/scaffolds/redux-store/store.ts +38 -0
  28. package/templates/scaffolds/rtk-query/baseApi.ts +24 -0
  29. package/templates/scaffolds/rtk-query/baseQuery.ts +12 -0
  30. package/templates/scaffolds/rtk-query/endpoints/api.one.ts +82 -0
  31. package/templates/scaffolds/rtk-query/endpoints/index.ts +1 -0
  32. package/templates/scaffolds/rtk-query/middlewares.ts +11 -0
  33. package/templates/scaffolds/rtk-query/queryTags.ts +13 -0
  34. package/templates/scaffolds/tanstack-query/api-base.ts +68 -68
  35. package/templates/scaffolds/tanstack-query/api-queries.ts +0 -19
  36. package/templates/scaffolds/tanstack-query/client.ts +8 -0
  37. package/templates/scaffolds/tanstack-query/module/index.ts +12 -12
  38. package/templates/scaffolds/tanstack-query/module/keys.ts +17 -17
  39. package/templates/scaffolds/tanstack-query/module/mappers.ts +15 -15
  40. package/templates/scaffolds/tanstack-query/module/mutations.ts +59 -55
  41. package/templates/scaffolds/tanstack-query/module/queries.ts +145 -156
  42. package/templates/scaffolds/tanstack-query/module/services.ts +74 -66
  43. package/templates/scaffolds/tanstack-router/layout/404.layout.tsx +3 -0
  44. package/templates/scaffolds/tanstack-router/layout/app.layout.tsx +10 -0
  45. package/templates/scaffolds/tanstack-router/layout/auth.layout.tsx +10 -0
  46. package/templates/scaffolds/tanstack-router/layout/error.layout.tsx +3 -0
  47. package/templates/scaffolds/tanstack-router/page/auth/login.tsx +3 -0
  48. package/templates/scaffolds/tanstack-router/page/one/index.tsx +3 -0
  49. package/templates/scaffolds/tanstack-router/page/one/one-id.tsx +128 -0
  50. package/templates/scaffolds/tanstack-router/router.ts +90 -0
  51. package/templates/scaffolds/tanstack-router/routes/_404.tsx +0 -0
  52. package/templates/scaffolds/tanstack-router/routes/__root.tsx +9 -0
  53. package/templates/scaffolds/tanstack-router/routes/_app.tsx +6 -0
  54. package/templates/scaffolds/tanstack-router/routes/_auth.tsx +6 -0
  55. package/templates/scaffolds/tanstack-router/routes/_error.tsx +0 -0
  56. package/templates/scaffolds/tanstack-router/routes/auth/login.tsx +6 -0
  57. package/templates/scaffolds/tanstack-router/routes/one/$id.tsx +191 -0
  58. package/templates/scaffolds/tanstack-router/routes/one/index.tsx +6 -0
  59. package/templates/scripts/setup.bat +284 -0
  60. package/templates/scripts/setup.ps1 +318 -0
@@ -0,0 +1,40 @@
1
+ export * as oneAPI from "./one.api";
2
+ /*
3
+
4
+ to call we simply do
5
+
6
+ import {oneAPI} from "@/api"
7
+
8
+ oneAPI.onePost({ payload: data });
9
+
10
+ oneAPI.oneGet();
11
+
12
+ oneAPI.oneGetId({ id: "1" });
13
+
14
+ * calling abort via signal
15
+ const controller = new AbortController();
16
+ oneAPI.oneGetIdParams({ id: "1", params: { search:"adam" }, signal: controller.signal, });
17
+
18
+ controller.abort();
19
+
20
+ * aborts request on unmount
21
+ useEffect(() => {
22
+ const controller = new AbortController();
23
+
24
+ oneAPI.oneGetIdParams({
25
+ id: "1",
26
+ params: { search: "adam" },
27
+ signal: controller.signal,
28
+ });
29
+
30
+ return () => {
31
+ controller.abort();
32
+ };
33
+ }, []);
34
+
35
+
36
+ oneAPI.onePut({ id: "1", payload: data });
37
+
38
+ oneAPI.oneDelete({ id: "1" });
39
+
40
+ */
@@ -0,0 +1,94 @@
1
+ import { ENDPOINTS } from "../endpoints";
2
+ import { CustomAxiosRequestConfig, request } from "../request";
3
+
4
+ type RequestOptions = Pick<
5
+ CustomAxiosRequestConfig,
6
+ "signal" | "skipErrorToast"
7
+ >;
8
+
9
+ export async function onePost(
10
+ { payload }: { payload: any },
11
+ options?: RequestOptions,
12
+ ) {
13
+ return await request({
14
+ url: ENDPOINTS.USER.BASE,
15
+ method: "POST",
16
+ data: payload,
17
+ ...options,
18
+ });
19
+ }
20
+
21
+ export async function onePostFile(
22
+ { payload }: { payload: any },
23
+ options?: RequestOptions,
24
+ ) {
25
+ return await request({
26
+ ...options,
27
+ url: ENDPOINTS.USER.BASE,
28
+ method: "POST",
29
+ data: payload,
30
+ headers: {
31
+ "Content-Type": "multipart/form-data",
32
+ },
33
+ });
34
+ }
35
+
36
+ export async function oneGet(options?: RequestOptions) {
37
+ return await request({
38
+ url: ENDPOINTS.USER.BASE,
39
+ method: "GET",
40
+ ...options,
41
+ });
42
+ }
43
+
44
+ export async function oneGetId(
45
+ { id }: { id: string },
46
+ options?: RequestOptions,
47
+ ) {
48
+ return await request({
49
+ url: ENDPOINTS.USER.BY_ID(id),
50
+ method: "GET",
51
+ ...options,
52
+ });
53
+ }
54
+
55
+ export async function oneGetIdParams(
56
+ {
57
+ id,
58
+ params,
59
+ }: {
60
+ id: string;
61
+ params: any;
62
+ },
63
+ options?: RequestOptions,
64
+ ) {
65
+ return await request({
66
+ url: ENDPOINTS.USER.BY_ID(id),
67
+ method: "GET",
68
+ params,
69
+ ...options,
70
+ });
71
+ }
72
+
73
+ export async function onePut(
74
+ { id, payload }: { id: string; payload: any },
75
+ options?: RequestOptions,
76
+ ) {
77
+ return await request({
78
+ url: ENDPOINTS.USER.BY_ID(id),
79
+ method: "PUT",
80
+ data: payload,
81
+ ...options,
82
+ });
83
+ }
84
+
85
+ export async function oneDelete(
86
+ { id }: { id: string },
87
+ options?: RequestOptions,
88
+ ) {
89
+ return await request({
90
+ url: ENDPOINTS.USER.BY_ID(id),
91
+ method: "DELETE",
92
+ ...options,
93
+ });
94
+ }
@@ -0,0 +1,9 @@
1
+ const createResourceEndpoints = (base: string) => ({
2
+ BASE: base,
3
+ BY_ID: (id: string) => `${base}/${id}`,
4
+ });
5
+
6
+ export const ENDPOINTS = {
7
+ USER: createResourceEndpoints("/user"),
8
+ CATEGORY_2: createResourceEndpoints("/category-2"),
9
+ };
@@ -0,0 +1,26 @@
1
+ import axios from "axios";
2
+
3
+ export const axiosInstance = axios.create({
4
+ baseURL: "http://localhost:5001",
5
+ timeout: 5000,
6
+ headers: {
7
+ "Content-Type": "application/json",
8
+ },
9
+ });
10
+
11
+ /*
12
+ const api = axios.create({ timeout: 5000 });
13
+ This specific request uses a 30-second timeout instead
14
+ await api.get("/slow-endpoint", { timeout: 30000 });
15
+ */
16
+
17
+ /*
18
+ TO integrate
19
+ 1. Generic response typing (HIGH PRIORITY)
20
+ 2. Error normalization utility
21
+ 3. Network error utility
22
+ 4. File upload helpers
23
+ 5. File download helpers
24
+ 6. Refresh-token queue
25
+ 7. Monitoring hooks
26
+ */
@@ -0,0 +1,103 @@
1
+ import { AxiosHeaders } from "axios";
2
+
3
+ import { axiosInstance } from ".";
4
+
5
+ let requestInterceptorId: number | null = null;
6
+ let responseInterceptorId: number | null = null;
7
+
8
+ export const setupInterceptors = ({
9
+ getAccessToken,
10
+ onUnauthorized,
11
+ }: {
12
+ getAccessToken: () => string;
13
+ onUnauthorized: () => void;
14
+ }) => {
15
+ if (requestInterceptorId !== null) {
16
+ axiosInstance.interceptors.request.eject(requestInterceptorId);
17
+ }
18
+
19
+ if (responseInterceptorId !== null) {
20
+ axiosInstance.interceptors.response.eject(responseInterceptorId);
21
+ }
22
+
23
+ requestInterceptorId = axiosInstance.interceptors.request.use(
24
+ (config) => {
25
+ const token = getAccessToken();
26
+
27
+ if (token) {
28
+ config.headers = AxiosHeaders.from(config.headers);
29
+ config.headers.set("Authorization", `Bearer ${token}`);
30
+ }
31
+
32
+ return config;
33
+ },
34
+ (error) => Promise.reject(error),
35
+ );
36
+
37
+ responseInterceptorId = axiosInstance.interceptors.response.use(
38
+ (response) => response,
39
+ (error) => {
40
+ if (error?.response?.status === 401) {
41
+ onUnauthorized?.();
42
+ }
43
+
44
+ /*
45
+ const skipToast = error?.config?.skipErrorToast;
46
+ if (!skipToast) {
47
+ const message =
48
+ error?.response?.data?.message ||
49
+ error?.message || "Something went wrong";
50
+ toast.error(message);
51
+ }
52
+
53
+
54
+ another method Handle only generic errors globally:
55
+ (error) => {
56
+ const status = error?.response?.status;
57
+
58
+ if (status >= 500) {
59
+ toast.error("Server error");
60
+ }
61
+
62
+ return Promise.reject(error);
63
+ }
64
+ And let components handle:
65
+ 400 Validation Error
66
+ 422 Form Error
67
+ 409 Duplicate Record
68
+ because those errors are usually specific to the screen.
69
+
70
+ A practical setup is:
71
+ Interceptor:
72
+ 401 → logout / redirect
73
+ 500 → generic toast
74
+
75
+ Component:
76
+ 400 → form validation
77
+ 404 → custom UI
78
+ 409 → business logic errors
79
+
80
+ This avoids flooding users with duplicate or overly generic error toasts while still preventing the UI from crashing.
81
+ */
82
+
83
+ return Promise.reject(error);
84
+ },
85
+ );
86
+ };
87
+
88
+ /*
89
+ Call `setupInterceptors(...)` once from your app entry file or root provider,
90
+ before any API request runs.
91
+
92
+ Call setupInterceptors(...) once in your app bootstrap, like App.tsx, main.tsx, or a root provider. That is the real integration step.
93
+
94
+ Example:
95
+ import { setupInterceptors } from "./templates/axios/interceptors";
96
+
97
+ setupInterceptors({
98
+ getAccessToken: () => localStorage.getItem("token") ?? "",
99
+ onUnauthorized: () => {
100
+ // logout / redirect here
101
+ },
102
+ });
103
+ */
@@ -0,0 +1,32 @@
1
+ import { AxiosRequestConfig, AxiosResponse } from "axios";
2
+
3
+ import { axiosInstance } from ".";
4
+
5
+ export interface CustomAxiosRequestConfig extends AxiosRequestConfig {
6
+ skipErrorToast?: boolean;
7
+ }
8
+
9
+ export const request = async ({
10
+ url,
11
+ method = "GET",
12
+ data,
13
+ params,
14
+ headers,
15
+ signal,
16
+ skipErrorToast,
17
+ ...config
18
+ }: CustomAxiosRequestConfig): Promise<AxiosResponse> => {
19
+ const axiosConfig: CustomAxiosRequestConfig = {
20
+ ...config,
21
+ url,
22
+ method,
23
+ data,
24
+ params,
25
+ headers,
26
+ signal,
27
+ skipErrorToast,
28
+ };
29
+
30
+ const response = await axiosInstance(axiosConfig as AxiosRequestConfig);
31
+ return response;
32
+ };
@@ -8,6 +8,10 @@ export function useRouterSearch<T extends Record<string, any>>({
8
8
  }) {
9
9
  const [searchParams, setSearchParams] = useSearchParams();
10
10
 
11
+ /*
12
+ * Keep defaults in a ref so setSearch always reads the latest defaults without
13
+ * forcing callbacks to be recreated. Prefer passing a stable default object.
14
+ */
11
15
  const defaultRef = useRef(defaultParams);
12
16
  defaultRef.current = defaultParams;
13
17
 
@@ -18,6 +22,7 @@ export function useRouterSearch<T extends Record<string, any>>({
18
22
  for (const key of keys) {
19
23
  const fallback = defaultRef.current[key];
20
24
  if (Array.isArray(fallback)) {
25
+ // Repeated URL keys are parsed back into arrays, e.g. ?color=red&color=blue.
21
26
  const val = searchParams.getAll(key);
22
27
  if (val.length > 0) {
23
28
  result[key as keyof T] = parseValue(val, fallback) as any;
@@ -43,9 +48,11 @@ export function useRouterSearch<T extends Record<string, any>>({
43
48
  for (const key of keys) {
44
49
  const value = updates[key as keyof T];
45
50
  if (value !== undefined) {
51
+ // null or the default value removes the key, keeping URLs shareable and minimal.
46
52
  if (value === null || areEqual(value, defaultRef.current[key])) {
47
53
  next.delete(key);
48
54
  } else if (Array.isArray(value)) {
55
+ // Delete first so array updates replace previous repeated keys instead of appending to them.
49
56
  next.delete(key);
50
57
  value.forEach((val) => {
51
58
  if (val !== undefined && val !== null && val !== "") {
@@ -78,6 +85,7 @@ export function useRouterSearch<T extends Record<string, any>>({
78
85
  function areEqual(a: any, b: any) {
79
86
  if (Array.isArray(a) && Array.isArray(b)) {
80
87
  if (a.length !== b.length) return false;
88
+ // Treat array query params as order-insensitive filter sets.
81
89
  const sortedA = [...a].sort();
82
90
  const sortedB = [...b].sort();
83
91
  return sortedA.every((val, index) => val === sortedB[index]);
@@ -46,6 +46,7 @@ export default function LoginPage() {
46
46
 
47
47
  const handleLoginSuccess = () => {
48
48
  // 1. Read the redirectTo value (e.g., "/page_two") or default to "/"
49
+ // In production, validate this stays an internal path before navigating.
49
50
  const destination = searchParams.get("redirectTo") || "/";
50
51
 
51
52
  // 2. Redirect the user back to their bookmarked page instead of the root page!
@@ -8,6 +8,10 @@ import { MainLayout } from "./layouts/main.layout";
8
8
 
9
9
  const PublicRoutes: RouteObject[] = [
10
10
  {
11
+ /*
12
+ # NOTE: Guard routes without a path run before their children.
13
+ This keeps auth decisions centralized while the nested layout controls shared UI.
14
+ */
11
15
  Component: PublicRoute,
12
16
  children: [
13
17
  {
@@ -29,6 +33,10 @@ const PublicRoutes: RouteObject[] = [
29
33
 
30
34
  const PrivateRoutes: RouteObject[] = [
31
35
  {
36
+ /*
37
+ # NOTE: PrivateRoute can block or redirect every route below it.
38
+ Any child route added here automatically inherits the same auth requirement.
39
+ */
32
40
  Component: PrivateRoute,
33
41
  children: [
34
42
  {
@@ -78,6 +86,7 @@ const NotFoundRoutes: RouteObject[] = [
78
86
  export const router = createBrowserRouter([
79
87
  ...PublicRoutes,
80
88
  ...PrivateRoutes,
89
+ // Keep the wildcard route last so specific public/private routes get matched first.
81
90
  ...NotFoundRoutes,
82
91
  ]);
83
92
 
@@ -181,6 +190,23 @@ export function useDocumentTitle(title: string) {
181
190
  2. **Why we use Component-level skeletons:**
182
191
  - **Data Loading:** Once the page code is downloaded and mounted, your API data fetching starts.
183
192
  - **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.
193
+
194
+ -------------------------------------------------------------------------------------
195
+
196
+ {
197
+ path: PATH.loads(),
198
+ handle: {
199
+ title: PAGE_TITLE.loads,
200
+ },
201
+ Component: LoadsPage,
202
+ }
203
+
204
+ import { useMatches } from "react-router";
205
+
206
+ const matches = useMatches();
207
+
208
+ const currentTitle =
209
+ matches[matches.length - 1]?.handle?.title ?? "";
184
210
  */
185
211
 
186
212
 
@@ -1,4 +1,4 @@
1
- import { Outlet, useRouteError } from "react-router-dom";
1
+ import { useRouteError } from "react-router-dom";
2
2
 
3
3
  export function ErrorLayout() {
4
4
  const error = useRouteError();
File without changes
@@ -0,0 +1,30 @@
1
+ import { combineReducers } from "@reduxjs/toolkit";
2
+ import oneReducer from "./slices/one.slice";
3
+ import twoReducer from "./slices/two.slice";
4
+ import { persistReducer } from "redux-persist";
5
+ import storageLocal from "redux-persist/lib/storage";
6
+ /* import storageSession from "redux-persist/lib/storage/session"; */
7
+
8
+ /*
9
+
10
+ # NOTE : combineReducers is used to combine multiple reducers into a single reducer
11
+ */
12
+
13
+ /* this is per slice config you have to define multiple if you have multi-slice persisted reducers */
14
+ const twoConfig = {
15
+ key: "two",
16
+ storage: storageLocal,
17
+ whitelist: [],
18
+ blacklist: [],
19
+ };
20
+ const persistedReducers = {
21
+ two: persistReducer(twoConfig, twoReducer),
22
+ };
23
+
24
+ const reducres = { one: oneReducer };
25
+ export const rootReducer = combineReducers({
26
+ ...reducres,
27
+ ...persistedReducers,
28
+ });
29
+ export type RootReducerState = ReturnType<typeof rootReducer>;
30
+ export default rootReducer;
@@ -0,0 +1,43 @@
1
+ import { createSelector } from "@reduxjs/toolkit";
2
+ import { RootState } from "../index";
3
+
4
+ export const selectOneSlice = (state: RootState) => state.one;
5
+ export const oneSelector = createSelector([selectOneSlice], (state) => {
6
+ if (state.oneData) {
7
+ return state.oneData;
8
+ } else {
9
+ return null;
10
+ }
11
+ });
12
+
13
+ /*
14
+
15
+ # NOTE : Its primary job is:
16
+ 1. Memoize expensive calculations
17
+ 2. Return stable references (same array/object if inputs didn't change)
18
+ 3. Prevent unnecessary recalculations and re-renders
19
+
20
+
21
+ now after this setp of selctor
22
+ we can handover all the states and memoised data to hook which can be called inside any number of component
23
+ it will save our time and code from repeating the code again and again
24
+
25
+ # NOTE : example
26
+
27
+ // hooks/useOne.ts
28
+ import { useSelector } from "react-redux";
29
+ import { selectOneSlice } from "../store/selector/one.selector";
30
+
31
+
32
+ export const useOne = () => {
33
+ const one = useSelector(selectOneSlice);
34
+ const { isLoading, isFetching, isError } = useSelector((state) => state.one);
35
+ any additional manipulation specific to component can be done here
36
+ return { one, isLoading, isFetching, isError };
37
+ };
38
+
39
+ usage in any component
40
+ import { useOne } from "../hooks/useOne";
41
+ const { one, isLoading, isFetching, isError } = useOne();
42
+
43
+ */
@@ -0,0 +1,11 @@
1
+ import { createSelector } from "@reduxjs/toolkit";
2
+ import { RootState } from "../index";
3
+
4
+ export const selectTwoSlice = (state: RootState) => state.two;
5
+ export const twoSelector = createSelector([selectTwoSlice], (state) => {
6
+ if (state.twoData) {
7
+ return state.twoData;
8
+ } else {
9
+ return null;
10
+ }
11
+ });