diff --git a/src/shared/sections/ErrorBoundary/index.ts b/src/shared/sections/ErrorBoundary/index.ts
new file mode 100644
index 0000000..a6ad481
--- /dev/null
+++ b/src/shared/sections/ErrorBoundary/index.ts
@@ -0,0 +1 @@
+export * from "./ErrorBoundary";
diff --git a/src/layout/Navbar/NavBar.tsx b/src/shared/sections/Navbar/NavBar.tsx
similarity index 98%
rename from src/layout/Navbar/NavBar.tsx
rename to src/shared/sections/Navbar/NavBar.tsx
index cdf5bae..1c1b553 100644
--- a/src/layout/Navbar/NavBar.tsx
+++ b/src/shared/sections/Navbar/NavBar.tsx
@@ -1,8 +1,8 @@
import React from "react";
-export interface NavBarProps {
+export type NavBarProps = {
setSidebarOpen?: (v: boolean) => void;
-}
+};
export const Navbar = ({ setSidebarOpen }: NavBarProps) => {
return (
diff --git a/src/layout/Navbar/index.ts b/src/shared/sections/Navbar/index.ts
similarity index 100%
rename from src/layout/Navbar/index.ts
rename to src/shared/sections/Navbar/index.ts
diff --git a/src/screens/NotFound.tsx b/src/shared/sections/NotFound/NotFound.tsx
similarity index 94%
rename from src/screens/NotFound.tsx
rename to src/shared/sections/NotFound/NotFound.tsx
index 1495f21..b92b9fe 100644
--- a/src/screens/NotFound.tsx
+++ b/src/shared/sections/NotFound/NotFound.tsx
@@ -2,7 +2,7 @@ import React from "react";
import { useNavigate } from "react-router-dom";
import { LightitLogo } from "~/assets";
-import { Button } from "~/shared";
+import { Button } from "~/components";
export const NotFound = () => {
const navigate = useNavigate();
diff --git a/src/shared/sections/NotFound/index.ts b/src/shared/sections/NotFound/index.ts
new file mode 100644
index 0000000..c7283ce
--- /dev/null
+++ b/src/shared/sections/NotFound/index.ts
@@ -0,0 +1 @@
+export * from "./NotFound";
diff --git a/src/layout/Sidebar/Sidebar.tsx b/src/shared/sections/Sidebar/Sidebar.tsx
similarity index 100%
rename from src/layout/Sidebar/Sidebar.tsx
rename to src/shared/sections/Sidebar/Sidebar.tsx
diff --git a/src/layout/Sidebar/index.ts b/src/shared/sections/Sidebar/index.ts
similarity index 100%
rename from src/layout/Sidebar/index.ts
rename to src/shared/sections/Sidebar/index.ts
diff --git a/src/shared/sections/index.ts b/src/shared/sections/index.ts
new file mode 100644
index 0000000..69813aa
--- /dev/null
+++ b/src/shared/sections/index.ts
@@ -0,0 +1,4 @@
+export * from "./ErrorBoundary";
+export * from "./Navbar";
+export * from "./NotFound";
+export * from "./Sidebar";
diff --git a/src/shared/utils/dateWithoutTimezone.ts b/src/shared/utils/dateWithoutTimezone.ts
new file mode 100644
index 0000000..78007eb
--- /dev/null
+++ b/src/shared/utils/dateWithoutTimezone.ts
@@ -0,0 +1,5 @@
+export const dateWithoutTimezone = (input: string) => {
+ const date = new Date(input).toISOString();
+ const withoutTimezone = date.substring(0, date.length - 1);
+ return new Date(withoutTimezone);
+};
diff --git a/src/shared/utils/index.ts b/src/shared/utils/index.ts
index 5571f66..b0554e9 100644
--- a/src/shared/utils/index.ts
+++ b/src/shared/utils/index.ts
@@ -1,3 +1,4 @@
export * from "./asyncTimeout";
+export * from "./dateWithoutTimezone";
export * from "./forwardRef";
export * from "./tw";
diff --git a/src/stores/index.ts b/src/stores/index.ts
index f23be27..fd73963 100644
--- a/src/stores/index.ts
+++ b/src/stores/index.ts
@@ -1 +1,2 @@
-export * from "./useUserStore";
+export * from "./useAuthStore";
+export * as exampleStore from "./useExampleStore";
diff --git a/src/stores/useAuthStore.ts b/src/stores/useAuthStore.ts
new file mode 100644
index 0000000..30f21f8
--- /dev/null
+++ b/src/stores/useAuthStore.ts
@@ -0,0 +1,34 @@
+/**
+ * This file represents an alternative pattern when using Zustand's `persist` middleware.
+ * In this case, we include actions within the store itself, as `persist` handles state persistence
+ * across sessions. Unlike our standard approach where actions are externalized, here we centralize
+ * state and actions for ease of persistence management.
+ *
+ * The `persist` middleware automatically saves the `token` to localStorage under the key "authStore".
+ * We only store the `token` here to comply with HIPAA regulations, ensuring that no sensitive user information
+ * beyond authentication tokens is persisted.
+ *
+ * This pattern is specifically used when persistence is required, overriding our standard practice of separating state and actions.
+ */
+
+import { create } from "zustand";
+import { persist } from "zustand/middleware";
+
+export type AuthStoreState = {
+ token: string | null;
+ setToken(token: string | null): void;
+};
+
+export const useAuthStore = create
()(
+ persist(
+ (set) => ({
+ token: null,
+ setToken: (token: string | null) => {
+ set(() => ({ token }));
+ },
+ }),
+ {
+ name: "authStore",
+ },
+ ),
+);
diff --git a/src/stores/useExampleStore.ts b/src/stores/useExampleStore.ts
new file mode 100644
index 0000000..3b89870
--- /dev/null
+++ b/src/stores/useExampleStore.ts
@@ -0,0 +1,34 @@
+/**
+ * This file adheres to our standard for Zustand stores without using `persist`.
+ * The state and actions are externalized, promoting modularity and separation of concerns,
+ * as outlined in Zustand's best practices: https://zustand.docs.pmnd.rs/guides/practice-with-no-store-actions.
+ *
+ * We use a namespace in the barrel file (e.g., export * as authStore from "./useAuthStore")
+ * to gather everything in one place, achieving both objectives: keeping concerns separate while avoiding
+ * multiple setters with similar names, as often seen in component states or sections.
+ *
+ * This structure simplifies state management and ensures consistency across the project.
+ */
+
+import { create } from "zustand";
+
+export type ExampleStoreState = {
+ firstValue: string | null;
+ secondValue: number | null;
+ thirdValue: number | null;
+};
+
+export const useStore = create()(() => ({
+ firstValue: null,
+ secondValue: null,
+ thirdValue: null,
+}));
+
+export const setFirstValue = () =>
+ useStore.setState((state) => ({ firstValue: state.firstValue }));
+
+export const setSecondValue = () =>
+ useStore.setState((state) => ({ secondValue: state.secondValue }));
+
+export const setThirdValue = () =>
+ useStore.setState((state) => ({ thirdValue: state.thirdValue }));
diff --git a/src/stores/useUserStore.ts b/src/stores/useUserStore.ts
deleted file mode 100644
index 1576556..0000000
--- a/src/stores/useUserStore.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { create } from "zustand";
-import { persist } from "zustand/middleware";
-
-export interface User {
- email: string;
- name: string;
-}
-
-export interface UserStoreState {
- user: User | null;
- setUser(user: User | null): void;
- token: string | null;
- setToken(token: string | null): void;
-}
-
-export const useUserStore = create()(
- persist(
- (set) => ({
- user: null,
- token: null,
- setUser: (user: User | null) => {
- set(() => ({ user }));
- },
- setToken: (token: string | null) => {
- set(() => ({ token }));
- },
- }),
- {
- name: "feedbackUserStore",
- },
- ),
-);
diff --git a/templates/domains/api/example.ts.hbs b/templates/domains/api/example.ts.hbs
new file mode 100644
index 0000000..9535847
--- /dev/null
+++ b/templates/domains/api/example.ts.hbs
@@ -0,0 +1,95 @@
+/**
+ * This file handles user-related API operations with runtime validation using Zod.
+ * Each function includes a type-safe schema (e.g., `{{camelCase name}}Schema`) for parsing and validating
+ * responses, ensuring that the data conforms to the expected structure at runtime.
+ *
+ * Zod is used here to provide robust validation and type inference, improving data integrity
+ * throughout the API response lifecycle.
+ *
+ * We preserve the `ServiceResponse` structure in the return value, allowing the original metadata
+ * (such as status, pagination, etc.) to pass through, while validating the actual `data` payload.
+ *
+ * The `URL.concat(["/", {{camelCase name}}Id].join(""))` approach is used for constructing dynamic URLs
+ * due to limitations with Handlebars, preventing the use of template literals (e.g., `${URL}/whateverId}`).
+ * However, the template literal format is preferred when possible for simplicity and readability.
+ */
+import { z } from "zod";
+
+import type { ServiceResponse } from "~/api";
+import { api } from "~/api";
+import { create{{camelCase name}}Schema, {{camelCase name}}Schema } from "./schemas";
+
+export type {{pascalCase name}} = z.infer;
+export type Create{{pascalCase name}} = z.infer;
+
+export type {{pascalCase name}}ListParams = {
+ page?: number;
+};
+
+const URL = "/{{kebabCase name}}";
+
+export const get{{pascalCase name}} = async (params: {{pascalCase name}}ListParams) => {
+ const { data } = await api.get>(URL, {
+ params,
+ });
+
+ // Runtime type check
+ const parsed = data.data.map((item) => {{camelCase name}}Schema.parse(item));
+
+ return {
+ ...data,
+ data: parsed,
+ };
+};
+
+export const get{{pascalCase name}}ById = async ({{camelCase name}}Id: string) => {
+ const { data } = await api.get>(
+ URL.concat(["/", {{camelCase name}}Id].join("")),
+ );
+
+ // Runtime type check
+ const parsed = {{camelCase name}}Schema.parse(data.data);
+
+ return {
+ ...data,
+ data: parsed,
+ };
+};
+
+export const create{{pascalCase name}} = async (body: Create{{pascalCase name}}) => {
+ const { data } = await api.post>(
+ URL,
+ body,
+ );
+
+ // Runtime type check
+ const parsed = {{camelCase name}}Schema.parse(data.data);
+
+ return {
+ ...data,
+ data: parsed,
+ };
+};
+
+export const update{{pascalCase name}} = async (body: {{pascalCase name}}) => {
+ const { data } = await api.put>(
+ URL.concat(["/", body.id].join("")),
+ body,
+ );
+
+ // Runtime type check
+ const parsed = {{camelCase name}}Schema.parse(data.data);
+
+ return {
+ ...data,
+ data: parsed,
+ };
+};
+
+export const delete{{pascalCase name}} = async ({{camelCase name}}Id: string) => {
+ const { data } = await api.delete>(
+ URL.concat(["/", {{camelCase name}}Id].join("")),
+ );
+
+ return data;
+};
diff --git a/templates/domains/api/index.ts.hbs b/templates/domains/api/index.ts.hbs
index f93dfbd..3e55cd5 100644
--- a/templates/domains/api/index.ts.hbs
+++ b/templates/domains/api/index.ts.hbs
@@ -1 +1,2 @@
-export * from "./example-api";
+export * from "./schemas";
+export * from "./{{kebabCase name}}";
diff --git a/templates/domains/api/schemas/example.hbs b/templates/domains/api/schemas/example.hbs
new file mode 100644
index 0000000..b9f26fc
--- /dev/null
+++ b/templates/domains/api/schemas/example.hbs
@@ -0,0 +1,9 @@
+import { z } from "zod";
+
+export const {{camelCase name}}Schema = z.object({
+ id: z.string(),
+ name: z.string(),
+ address: z.string(),
+});
+
+export const create{{camelCase name}}Schema = {{camelCase name}}Schema.omit({ id: true });
diff --git a/templates/domains/api/schemas/index.hbs b/templates/domains/api/schemas/index.hbs
new file mode 100644
index 0000000..263716e
--- /dev/null
+++ b/templates/domains/api/schemas/index.hbs
@@ -0,0 +1 @@
+export * from "./{{camelCase name}}Schemas";
diff --git a/templates/domains/context/context.hbs b/templates/domains/context/context.hbs
new file mode 100644
index 0000000..8c911fe
--- /dev/null
+++ b/templates/domains/context/context.hbs
@@ -0,0 +1,5 @@
+import * as React from "react"
+
+export const use{{pascalCase name}}Context: React.FC = () => {
+ return This is an Component Example.
+}
diff --git a/templates/domains/context/index.ts.hbs b/templates/domains/context/index.ts.hbs
index 17b46f9..6f16fdc 100644
--- a/templates/domains/context/index.ts.hbs
+++ b/templates/domains/context/index.ts.hbs
@@ -1 +1 @@
-export * from "./useExampleContext";
+export * from "./use{{pascalCase name}}Context";
diff --git a/templates/domains/domain-barrel.ts.hbs b/templates/domains/domain-barrel.ts.hbs
index 84f4bad..03f8044 100644
--- a/templates/domains/domain-barrel.ts.hbs
+++ b/templates/domains/domain-barrel.ts.hbs
@@ -1 +1 @@
-export * from "./screens"
+export * from "./{{pascalCase name}}Router.tsx";
diff --git a/templates/domains/queries/index.ts.hbs b/templates/domains/queries/index.ts.hbs
index 4ac284e..35793da 100644
--- a/templates/domains/queries/index.ts.hbs
+++ b/templates/domains/queries/index.ts.hbs
@@ -1 +1 @@
-export * from "./useSomethingQueryExample";
+export * from "./{{kebabCase name}}";
diff --git a/templates/domains/queries/query-example.tsx.hbs b/templates/domains/queries/query-example.tsx.hbs
index 11a7a4a..7344a7f 100644
--- a/templates/domains/queries/query-example.tsx.hbs
+++ b/templates/domains/queries/query-example.tsx.hbs
@@ -1,5 +1,144 @@
-import * as React from "react"
+import { createQueryKeys } from "@lukemorales/query-key-factory";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-export const useSomethingQueryExample = () => {
- return This is a useQueryExample component.
+import type {
+ {{pascalCase name}},
+ {{pascalCase name}}ListParams,
+} from "../api/{{kebabCase name}}";
+import {
+ create{{pascalCase name}},
+ delete{{pascalCase name}},
+ get{{pascalCase name}}ById,
+ get{{pascalCase name}},
+ update{{pascalCase name}},
+} from "../api/{{kebabCase name}}";
+
+const {{camelCase name}}Keys = createQueryKeys("{{camelCase name}}", {
+ list: {
+ queryKey: null,
+ queryFn: () => get{{pascalCase name}},
+ },
+ detail: ({{camelCase name}}Id: string) => ({
+ queryKey: [{{camelCase name}}Id],
+ queryFn: () => get{{pascalCase name}}ById({{camelCase name}}Id),
+ }),
+});
+
+const use{{pascalCase name}}Query = (params: {{pascalCase name}}ListParams) =>
+useQuery({
+ ...{{camelCase name}}Keys.list,
+ queryFn: () => get{{pascalCase name}}(params),
+});
+
+const use{{pascalCase name}}DetailQuery = ({{camelCase name}}Id: string) =>
+ useQuery({{camelCase name}}Keys.detail({{camelCase name}}Id));
+
+const usePrefetch{{pascalCase name}}DetailQuery = ({{camelCase name}}Id: string) => {
+ const queryClient = useQueryClient();
+ return queryClient.prefetchQuery({{camelCase name}}Keys.detail({{camelCase name}}Id));
+}
+
+const useCreate{{pascalCase name}}Mutation = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: create{{pascalCase name}},
+ onSuccess: (new{{pascalCase name}}) => {
+ queryClient.setQueryData(
+ {{camelCase name}}Keys.detail(new{{pascalCase name}}.data.id).queryKey,
+ new{{pascalCase name}},
+ );
+
+ void queryClient.invalidateQueries({
+ queryKey: {{camelCase name}}Keys._def,
+ });
+ },
+ });
+}
+const useUpdate{{pascalCase name}}Mutation = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: update{{pascalCase name}},
+ onMutate: async (new{{pascalCase name}}) => {
+ // Cancel any outgoing refetches (so they don't overwrite our optimistic update)
+ await queryClient.cancelQueries({{camelCase name}}Keys.detail(new{{pascalCase name}}.id));
+
+ // Snapshot the previous value
+ const previousTasks = queryClient.getQueryData(
+ {{camelCase name}}Keys.detail(new{{pascalCase name}}.id).queryKey,
+ );
+
+ // Optimistically update to the new value
+ queryClient.setQueryData(
+ {{camelCase name}}Keys.detail(new{{pascalCase name}}.id).queryKey,
+ new{{pascalCase name}},
+ );
+
+ // Return a context object with the snapshotted value
+ return { previousTasks };
+ },
+ onError: (_err, {{camelCase name}}, context) => {
+ queryClient.setQueryData(
+ {{camelCase name}}Keys.detail({{camelCase name}}.id).queryKey,
+ context?.previousTasks,
+ );
+ },
+ onSuccess: (new{{pascalCase name}}) => {
+ queryClient.setQueryData(
+ {{camelCase name}}Keys.detail(new{{pascalCase name}}.data.id).queryKey,
+ new{{pascalCase name}},
+ );
+
+ void queryClient.invalidateQueries({
+ queryKey: {{camelCase name}}Keys._def,
+ });
+ },
+ });
+}
+
+const useDelete{{pascalCase name}}Mutation = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: delete{{pascalCase name}},
+ onMutate: async ({{camelCase name}}Id) => {
+ // Cancel any outgoing refetches (so they don't overwrite our optimistic update)
+ await queryClient.cancelQueries({{camelCase name}}Keys.detail({{camelCase name}}Id));
+
+ // Snapshot the previous value
+ const previousTasks = queryClient.getQueryData(
+ {{camelCase name}}Keys.detail({{camelCase name}}Id).queryKey,
+ );
+
+ // Optimistically update to the new value
+ queryClient.setQueryData(
+ {{camelCase name}}Keys.detail({{camelCase name}}Id).queryKey,
+ (old: {{pascalCase name}}[]) =>
+ old.filter((t: {{pascalCase name}}) => t.id !== {{camelCase name}}Id),
+ );
+
+ // Return a context object with the snapshotted value
+ return { previousTasks };
+ },
+ onError: (_err, {{camelCase name}}Id, context) => {
+ queryClient.setQueryData(
+ {{camelCase name}}Keys.detail({{camelCase name}}Id).queryKey,
+ context?.previousTasks,
+ );
+ },
+ onSuccess: () => {
+ void queryClient.invalidateQueries({
+ queryKey: {{camelCase name}}Keys._def,
+ });
+ },
+ });
}
+
+
+
+export {
+ use{{pascalCase name}}Query,
+ use{{pascalCase name}}DetailQuery,
+ usePrefetch{{pascalCase name}}DetailQuery,
+ useCreate{{pascalCase name}}Mutation,
+ useUpdate{{pascalCase name}}Mutation,
+ useDelete{{pascalCase name}}Mutation,
+};
diff --git a/templates/domains/router.tsx.hbs b/templates/domains/router.tsx.hbs
new file mode 100644
index 0000000..497f1ac
--- /dev/null
+++ b/templates/domains/router.tsx.hbs
@@ -0,0 +1,13 @@
+import React from "react";
+import { Route } from "react-router-dom";
+
+import { RouterWrapper } from "~/router";
+import { ScreenExample } from "./screens";
+
+export const {{pascalCase name}}Router = () => {
+ return (
+
+ } path="/{{kebabCase name}}" />
+
+ );
+};
diff --git a/templates/domains/screens/index.ts.hbs b/templates/domains/screens/index.ts.hbs
index 0abe9af..64e5934 100644
--- a/templates/domains/screens/index.ts.hbs
+++ b/templates/domains/screens/index.ts.hbs
@@ -1 +1 @@
-export * from "./ExampleScreen";
+export * from "./ScreenExample";
diff --git a/templates/domains/sections/index.ts.hbs b/templates/domains/sections/index.ts.hbs
index e054394..0e4c858 100644
--- a/templates/domains/sections/index.ts.hbs
+++ b/templates/domains/sections/index.ts.hbs
@@ -1 +1 @@
-export * from "./ExampleSection";
+export * from "./SectionExample";
diff --git a/tsconfig.json b/tsconfig.json
index cc6d06f..fa56153 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -9,6 +9,12 @@
"skipLibCheck": true,
"baseUrl": "./src",
"paths": {
+ "~/assets": ["./shared/assets"],
+ "~/components": ["./shared/components/ui"],
+ "~/hooks": ["./shared/hooks"],
+ "~/icons": ["./shared/components/icons"],
+ "~/sections": ["./shared/sections"],
+ "~/utils": ["./shared/utils"],
"~/*": ["*"]
},
@@ -27,7 +33,7 @@
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
- "include": ["./"],
+ "include": ["./src/**/*"],
"references": [{ "path": "./tsconfig.node.json" }],
"exclude": ["node_modules", "build", "dist", ".next", ".expo"]
}
diff --git a/vite.config.ts b/vite.config.ts
index 1b30361..c4938e2 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -34,7 +34,33 @@ const config = ({ mode }: ConfigEnv): UserConfigExport => {
origin: VITE_APP_ENV === "local" ? VITE_APP_URL : "",
},
resolve: {
- alias: [{ find: "~", replacement: path.resolve(__dirname, "./src") }],
+ alias: [
+ {
+ find: "~/assets",
+ replacement: path.resolve(__dirname, "./src/shared/assets"),
+ },
+ {
+ find: "~/components",
+ replacement: path.resolve(__dirname, "./src/shared/components/ui"),
+ },
+ {
+ find: "~/hooks",
+ replacement: path.resolve(__dirname, "./src/shared/hooks"),
+ },
+ {
+ find: "~/icons",
+ replacement: path.resolve(__dirname, "./src/shared/components/icons"),
+ },
+ {
+ find: "~/sections",
+ replacement: path.resolve(__dirname, "./src/shared/sections"),
+ },
+ {
+ find: "~/utils",
+ replacement: path.resolve(__dirname, "./src/shared/utils"),
+ },
+ { find: "~", replacement: path.resolve(__dirname, "./src") },
+ ],
},
};