+ {/* TODO: deprecated */}
}
- />
-
- }
/>
diff --git a/resources/js/router/ProtectedRoute.tsx b/resources/js/router/ProtectedRoute.tsx
index 31fc3d84002..456693104fc 100644
--- a/resources/js/router/ProtectedRoute.tsx
+++ b/resources/js/router/ProtectedRoute.tsx
@@ -1,7 +1,7 @@
import type { ReactNode } from "react";
import { Navigate, Outlet } from "react-router-dom";
-import { useUserStore } from "@/stores/useUserStore";
+import { useUserStore } from "~/stores/useUserStore";
import { ROUTES } from "./routes";
type UserState = "loggedOut" | "standard" | "admin";
@@ -20,7 +20,7 @@ export const ProtectedRoute = ({
expected: UserState | UserState[];
}) => {
const userState = useUserStore((state) =>
- state.token ? state.user?.role ?? "standard" : "loggedOut",
+ state.token ? (state.user?.role ?? "standard") : "loggedOut",
);
if (!expected.includes(userState)) {
diff --git a/resources/js/router/Router.tsx b/resources/js/router/Router.tsx
index db6a9c0fc81..9c970c882f7 100644
--- a/resources/js/router/Router.tsx
+++ b/resources/js/router/Router.tsx
@@ -1,52 +1,16 @@
-import { Navigate, Route, Routes, useLocation } from "react-router-dom";
-import type { Location } from "react-router-dom";
+import React from "react";
+import { BrowserRouter, Route, Routes } from "react-router-dom";
-import { Layout } from "@/layout";
-import { Home, NotFound, Users } from "@/screens";
-import { Login } from "@/screens/Login";
-import { ModalRouter } from "./ModalRouter";
-import { ProtectedRoute } from "./ProtectedRoute";
-import { ROUTES } from "./routes";
+import { GuestRouter } from "~/domains";
+import { NotFound } from "~/sections";
export const Router = () => {
- const location = useLocation();
- const { previousLocation } = (location.state ?? {}) as {
- previousLocation?: Location;
- };
-
return (
- <>
- {/* PUBLIC ONLY ROUTES */}
-
- }>
- } path={ROUTES.login} />
-
-
- {/* PRIVATE ONLY ROUTES */}
- }>
- }>
- } path={ROUTES.base} />
-
- } path={ROUTES.home} />
-
- } />
-
-
-
- }>
- }>
- } path={ROUTES.users} />
-
-
-
-
- {/* MODALS ROUTES */}
+
+
- }
- />
+ } />
- >
+
);
};
diff --git a/resources/js/router/components/RouterWrapper/RouterWrapper.tsx b/resources/js/router/components/RouterWrapper/RouterWrapper.tsx
new file mode 100644
index 00000000000..308a0c11ec4
--- /dev/null
+++ b/resources/js/router/components/RouterWrapper/RouterWrapper.tsx
@@ -0,0 +1,24 @@
+import type { PropsWithChildren } from "react";
+import React from "react";
+import { Navigate, Route, Routes } from "react-router-dom";
+
+import { MainLayout } from "~/router";
+import { useAuthStore } from "~/stores";
+
+type Props = PropsWithChildren & {
+ guest?: boolean;
+};
+
+export const RouterWrapper = ({ guest = false, children }: Props) => {
+ const isAuthenticated = useAuthStore((state) => state.token);
+
+ if (!guest && !isAuthenticated) {
+ return
;
+ }
+
+ return (
+
+ }>{children}
+
+ );
+};
diff --git a/resources/js/router/components/RouterWrapper/index.ts b/resources/js/router/components/RouterWrapper/index.ts
new file mode 100644
index 00000000000..58dd8004886
--- /dev/null
+++ b/resources/js/router/components/RouterWrapper/index.ts
@@ -0,0 +1 @@
+export * from "./RouterWrapper";
diff --git a/resources/js/router/components/index.ts b/resources/js/router/components/index.ts
new file mode 100644
index 00000000000..58dd8004886
--- /dev/null
+++ b/resources/js/router/components/index.ts
@@ -0,0 +1 @@
+export * from "./RouterWrapper";
diff --git a/resources/js/router/constants/index.ts b/resources/js/router/constants/index.ts
new file mode 100644
index 00000000000..9bf9b1b68aa
--- /dev/null
+++ b/resources/js/router/constants/index.ts
@@ -0,0 +1 @@
+export * from "./routes";
diff --git a/resources/js/router/constants/routes.ts b/resources/js/router/constants/routes.ts
new file mode 100644
index 00000000000..a6eeef80abc
--- /dev/null
+++ b/resources/js/router/constants/routes.ts
@@ -0,0 +1,10 @@
+export const ROUTES = {
+ home: "/",
+ login: "/login",
+ example: "/example",
+ users: "/users",
+} as const;
+
+export const MODAL_ROUTES = {
+ successModal: "/successModal",
+} as const;
diff --git a/resources/js/router/index.ts b/resources/js/router/index.ts
index 208b44dbc28..21b27143919 100644
--- a/resources/js/router/index.ts
+++ b/resources/js/router/index.ts
@@ -1,3 +1,5 @@
-export * from "./ProtectedRoute";
export * from "./Router";
-export * from "./routes";
+export * from "./components";
+export * from "./constants";
+export * from "./layouts";
+export * from "./types";
diff --git a/resources/js/router/layouts/MainLayout/MainLayout.tsx b/resources/js/router/layouts/MainLayout/MainLayout.tsx
new file mode 100644
index 00000000000..64d1836000f
--- /dev/null
+++ b/resources/js/router/layouts/MainLayout/MainLayout.tsx
@@ -0,0 +1,19 @@
+import { Outlet } from "react-router-dom";
+
+import { Navbar, Sidebar } from "~/sections";
+
+export const MainLayout = () => {
+ return (
+
+ );
+};
diff --git a/resources/js/router/layouts/MainLayout/index.ts b/resources/js/router/layouts/MainLayout/index.ts
new file mode 100644
index 00000000000..1268cf924f4
--- /dev/null
+++ b/resources/js/router/layouts/MainLayout/index.ts
@@ -0,0 +1 @@
+export * from "./MainLayout";
diff --git a/resources/js/router/layouts/index.ts b/resources/js/router/layouts/index.ts
new file mode 100644
index 00000000000..1268cf924f4
--- /dev/null
+++ b/resources/js/router/layouts/index.ts
@@ -0,0 +1 @@
+export * from "./MainLayout";
diff --git a/resources/js/router/routes.ts b/resources/js/router/routes.ts
index 462fce0ffcc..35f309d5895 100644
--- a/resources/js/router/routes.ts
+++ b/resources/js/router/routes.ts
@@ -5,8 +5,3 @@ export const ROUTES = {
users: "/users",
notFound: "*",
} as const;
-
-export const MODAL_ROUTES = {
- exampleModal: "/example-modal",
- userForm: "/user-form",
-} as const;
diff --git a/resources/js/router/types/index.ts b/resources/js/router/types/index.ts
new file mode 100644
index 00000000000..97403fbeecf
--- /dev/null
+++ b/resources/js/router/types/index.ts
@@ -0,0 +1,14 @@
+import type { FC } from "react";
+
+export interface BaseRoute {
+ screen: FC;
+ layout?: FC;
+ path: string;
+ // Navigation Purposes
+ name: string;
+ nav: boolean;
+ // Revise the type of the `guest` property
+ // Redirecting purposes
+ guest: boolean;
+ permissions: string[];
+}
diff --git a/resources/js/router/useNavigateModal.ts b/resources/js/router/useNavigateModal.ts
deleted file mode 100644
index fe1711147e8..00000000000
--- a/resources/js/router/useNavigateModal.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { useLocation, useNavigate } from "react-router-dom";
-
-import type { MODAL_ROUTES } from "./routes";
-
-type ModalRoutes = (typeof MODAL_ROUTES)[keyof typeof MODAL_ROUTES];
-type ValidModalUrl
= T extends `${infer _}/${infer _}`
- ? never
- : ModalRoutes | `${ModalRoutes}/${T}` | `${ModalRoutes}/${T}/${T}`;
-
-export const useNavigateModal = () => {
- const location = useLocation();
- const navigate = useNavigate();
-
- const { previousLocation } = (location.state ?? {}) as {
- previousLocation?: Location;
- };
-
- // we make normal routing work as well as param routing, but make multiple params invalid
- return (
- url: ValidModalUrl,
- state?: Record,
- ) => {
- navigate(url, {
- state: { ...state, previousLocation: previousLocation ?? location },
- });
- };
-};
diff --git a/resources/js/screens/ErrorBoundaryFallback.tsx b/resources/js/screens/ErrorBoundaryFallback.tsx
index 3cd464452f6..7a6a2aef12e 100644
--- a/resources/js/screens/ErrorBoundaryFallback.tsx
+++ b/resources/js/screens/ErrorBoundaryFallback.tsx
@@ -1,4 +1,4 @@
-import { Logo } from "@/components";
+import { Logo } from "~/icons";
export const ErrorBoundaryFallback = () => {
return (
diff --git a/resources/js/screens/Home.tsx b/resources/js/screens/Home.tsx
index e2e48fdd22b..cfa78b42084 100644
--- a/resources/js/screens/Home.tsx
+++ b/resources/js/screens/Home.tsx
@@ -1,9 +1,4 @@
-import { MODAL_ROUTES } from "@/router";
-import { useNavigateModal } from "@/router/useNavigateModal";
-import { Button } from "@/ui";
-
export const Home = () => {
- const navigateModal = useNavigateModal();
return (
HOME Title
@@ -23,10 +18,6 @@ export const Home = () => {
item
-
-
);
};
diff --git a/resources/js/screens/Login.tsx b/resources/js/screens/Login.tsx
index e16e846ea48..98b5fcc76e6 100644
--- a/resources/js/screens/Login.tsx
+++ b/resources/js/screens/Login.tsx
@@ -3,11 +3,11 @@ import { useMutation } from "@tanstack/react-query";
import { jwtDecode } from "jwt-decode";
import { useNavigate } from "react-router-dom";
-import { googleLogin } from "@/api";
-import { Logo } from "@/components";
-import { ROUTES } from "@/router";
-import { useUserStore } from "@/stores";
-import { errorToast, useToastStore } from "@/ui";
+import { googleLogin } from "~/api";
+import { Logo } from "~/icons";
+import { ROUTES } from "~/router";
+import { useUserStore } from "~/stores";
+import { errorToast, useToastStore } from "~/ui";
export const Login = () => {
const { pushToast } = useToastStore();
@@ -20,7 +20,7 @@ export const Login = () => {
onSuccess: (data) => {
void pushToast({ type: "success", title: "Welcome back!" });
setToken(data.data.accessToken);
- navigate(ROUTES.base);
+ navigate(ROUTES.home);
},
onError: (e) => {
errorToast(e);
@@ -29,7 +29,7 @@ export const Login = () => {
// because we KNOW the login will fail
void pushToast({ type: "success", title: "Welcome back!" });
setToken("some token");
- navigate(ROUTES.base);
+ navigate(ROUTES.home);
},
});
diff --git a/resources/js/screens/NotFound.tsx b/resources/js/screens/NotFound.tsx
index bf7fbd24a2e..20d1670ef6e 100644
--- a/resources/js/screens/NotFound.tsx
+++ b/resources/js/screens/NotFound.tsx
@@ -1,6 +1,6 @@
import { Link } from "react-router-dom";
-import { ROUTES } from "@/router";
+import { ROUTES } from "~/router";
export const NotFound = () => {
return (
@@ -23,7 +23,7 @@ export const NotFound = () => {
← Back to home
diff --git a/resources/js/screens/Users/Users.tsx b/resources/js/screens/Users/Users.tsx
index 343a9b5beda..915f3d769c9 100644
--- a/resources/js/screens/Users/Users.tsx
+++ b/resources/js/screens/Users/Users.tsx
@@ -1,10 +1,8 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import { deleteUser, getUsersQuery } from "@/api";
-import { MODAL_ROUTES } from "@/router";
-import { useNavigateModal } from "@/router/useNavigateModal";
-import { Button, errorToast, icons, useToastStore } from "@/ui";
-import { tw } from "@/utils";
+import { deleteUser, getUsersQuery } from "~/api";
+import { Button, errorToast, icons, useToastStore } from "~/ui";
+import { tw } from "~/utils";
const statuses = {
Completed: "text-green-400 bg-green-400/10",
@@ -153,15 +151,14 @@ export const Users = () => {
onError: errorToast,
});
- const navigateModal = useNavigateModal();
-
return (
Latest activity
diff --git a/resources/js/shared/assets/index.ts b/resources/js/shared/assets/index.ts
new file mode 100644
index 00000000000..c2d38d9e90d
--- /dev/null
+++ b/resources/js/shared/assets/index.ts
@@ -0,0 +1 @@
+export { default as LightitLogo } from "./lightit-logo-violet.svg";
diff --git a/resources/js/shared/assets/lightit-logo-violet.svg b/resources/js/shared/assets/lightit-logo-violet.svg
new file mode 100644
index 00000000000..a12db0c8562
--- /dev/null
+++ b/resources/js/shared/assets/lightit-logo-violet.svg
@@ -0,0 +1,19 @@
+
diff --git a/resources/js/shared/components/icons/CloseIcon.tsx b/resources/js/shared/components/icons/CloseIcon.tsx
new file mode 100644
index 00000000000..87885df814e
--- /dev/null
+++ b/resources/js/shared/components/icons/CloseIcon.tsx
@@ -0,0 +1,34 @@
+import React from "react";
+
+import type { SVGProps } from "~/config";
+
+export const CloseIcon = ({ className, ...props }: SVGProps) => (
+
+);
+
+export const CloseCircleIcon = ({ className, ...props }: SVGProps) => (
+
+);
diff --git a/resources/js/components/Logo.tsx b/resources/js/shared/components/icons/Logo.tsx
similarity index 99%
rename from resources/js/components/Logo.tsx
rename to resources/js/shared/components/icons/Logo.tsx
index b948f917150..0b08d061ae5 100644
--- a/resources/js/components/Logo.tsx
+++ b/resources/js/shared/components/icons/Logo.tsx
@@ -1,4 +1,4 @@
-import type { SVGProps } from "@/shared.types";
+import type { SVGProps } from "~/shared.types";
export const Logo = ({ className, ...props }: SVGProps) => {
return (
diff --git a/resources/js/shared/components/icons/index.ts b/resources/js/shared/components/icons/index.ts
new file mode 100644
index 00000000000..55042c3f751
--- /dev/null
+++ b/resources/js/shared/components/icons/index.ts
@@ -0,0 +1,2 @@
+export * from "./CloseIcon";
+export * from "./Logo";
diff --git a/resources/js/shared/components/index.ts b/resources/js/shared/components/index.ts
new file mode 100644
index 00000000000..ac3652643a5
--- /dev/null
+++ b/resources/js/shared/components/index.ts
@@ -0,0 +1,2 @@
+export * from "./icons";
+export * from "./ui";
diff --git a/resources/js/shared/components/ui/Button/.DS_Store b/resources/js/shared/components/ui/Button/.DS_Store
new file mode 100644
index 00000000000..b1815ee5986
Binary files /dev/null and b/resources/js/shared/components/ui/Button/.DS_Store differ
diff --git a/resources/js/shared/components/ui/Button/Button.stories.tsx b/resources/js/shared/components/ui/Button/Button.stories.tsx
new file mode 100644
index 00000000000..01663c9e92a
--- /dev/null
+++ b/resources/js/shared/components/ui/Button/Button.stories.tsx
@@ -0,0 +1,53 @@
+import type { Meta, StoryObj } from "@storybook/react";
+
+import { Button } from "./Button";
+
+const meta: Meta = {
+ title: "Components/Button",
+ component: Button,
+ parameters: {
+ componentSubtitle: "A simple button component",
+ },
+ tags: ["autodocs"],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const DefaultButton: Story = {
+ args: {
+ variant: "default",
+ size: "default",
+ children: "Click Me",
+ disabled: false,
+ },
+};
+
+export const ErrorButton: Story = {
+ args: {
+ ...DefaultButton.args,
+ variant: "error",
+ },
+};
+
+export const SuccessButton: Story = {
+ args: {
+ ...DefaultButton.args,
+ variant: "success",
+ },
+};
+
+export const InfoButton: Story = {
+ args: {
+ ...DefaultButton.args,
+ variant: "info",
+ },
+};
+
+export const DisabledButton: Story = {
+ args: {
+ ...DefaultButton.args,
+ disabled: true,
+ },
+};
diff --git a/resources/js/shared/components/ui/Button/Button.tsx b/resources/js/shared/components/ui/Button/Button.tsx
new file mode 100644
index 00000000000..74f15998e16
--- /dev/null
+++ b/resources/js/shared/components/ui/Button/Button.tsx
@@ -0,0 +1,50 @@
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { tv } from "tailwind-variants";
+import type { VariantProps } from "tailwind-variants";
+
+const buttonVariants = tv({
+ base: "focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium text-white ring-gray-900 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
+ variants: {
+ variant: {
+ default: "bg-purple-700 hover:bg-purple-500",
+ error: "bg-red-700 hover:bg-red-500",
+ info: "bg-blue-700 hover:bg-blue-500",
+ success: "bg-green-700 hover:bg-green-500",
+ },
+ size: {
+ default: "px-4 py-3 text-base",
+ sm: "px-4 py-2 text-sm",
+ icon: "h-10 w-10",
+ },
+ disabled: {
+ true: "pointer-events-none bg-gray-500",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ disabled: false,
+ },
+});
+
+export type ButtonProps = {
+ asChild?: boolean;
+} & React.ComponentProps<"button"> &
+ VariantProps;
+
+const Button = React.forwardRef, ButtonProps>(
+ ({ className, variant, size, disabled, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button";
+ return (
+
+ );
+ },
+);
+Button.displayName = "Button";
+
+export { Button, buttonVariants };
diff --git a/resources/js/shared/components/ui/Button/index.ts b/resources/js/shared/components/ui/Button/index.ts
new file mode 100644
index 00000000000..e22c29adcf9
--- /dev/null
+++ b/resources/js/shared/components/ui/Button/index.ts
@@ -0,0 +1 @@
+export * from "./Button";
diff --git a/resources/js/shared/components/ui/IconWrapper/IconWrapper.tsx b/resources/js/shared/components/ui/IconWrapper/IconWrapper.tsx
new file mode 100644
index 00000000000..5cb0f46e39d
--- /dev/null
+++ b/resources/js/shared/components/ui/IconWrapper/IconWrapper.tsx
@@ -0,0 +1,29 @@
+import React from "react";
+
+import { tw } from "~/shared";
+
+export const IconWrapper = ({
+ size = "md",
+ className,
+ style,
+ children,
+}: {
+ size?: "sm" | "md" | "lg" | "xl";
+ className?: string;
+ style?: React.CSSProperties;
+ children: React.ReactNode;
+}) => (
+
+ {children}
+
+);
diff --git a/resources/js/shared/components/ui/IconWrapper/index.ts b/resources/js/shared/components/ui/IconWrapper/index.ts
new file mode 100644
index 00000000000..65e9f085cdc
--- /dev/null
+++ b/resources/js/shared/components/ui/IconWrapper/index.ts
@@ -0,0 +1 @@
+export * from "./IconWrapper";
diff --git a/resources/js/shared/components/ui/Modal/Modal.tsx b/resources/js/shared/components/ui/Modal/Modal.tsx
new file mode 100644
index 00000000000..090e175659c
--- /dev/null
+++ b/resources/js/shared/components/ui/Modal/Modal.tsx
@@ -0,0 +1,124 @@
+import React, { forwardRef } from "react";
+import type { ComponentProps, ElementRef } from "react";
+import * as DialogPrimitive from "@radix-ui/react-dialog";
+
+import { CloseIcon, tw } from "~/shared";
+
+const Root = DialogPrimitive.Root;
+Root.displayName = "Modal.Root";
+
+const Trigger = DialogPrimitive.Trigger;
+Trigger.displayName = "Modal.Trigger";
+const Overlay = forwardRef<
+ ElementRef,
+ ComponentProps
+>(({ className, ...props }, ref) => (
+
+));
+Overlay.displayName = "Modal.Overlay";
+
+const CloseButton = forwardRef<
+ ElementRef,
+ ComponentProps
+>(({ className, ...props }, ref) => (
+
+
+ Close
+
+));
+CloseButton.displayName = "Modal.CloseButton";
+
+const Content = forwardRef<
+ ElementRef,
+ ComponentProps
+>(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+));
+Content.displayName = DialogPrimitive.Content.displayName;
+
+const Header = ({ className, ...props }: ComponentProps<"div">) => (
+
+);
+Header.displayName = "Modal.Header";
+
+const Footer = ({ className, ...props }: ComponentProps<"div">) => (
+
+);
+Footer.displayName = "Modal.Footer";
+
+const Title = forwardRef<
+ ElementRef,
+ ComponentProps
+>(({ className, ...props }, ref) => (
+
+));
+Title.displayName = "Modal.Title";
+
+const Description = forwardRef<
+ ElementRef,
+ ComponentProps
+>(({ className, ...props }, ref) => (
+
+));
+Description.displayName = "Modal.Description";
+
+export {
+ Root,
+ Overlay,
+ Trigger,
+ CloseButton,
+ Content,
+ Header,
+ Footer,
+ Title,
+ Description,
+};
diff --git a/resources/js/shared/components/ui/Modal/index.ts b/resources/js/shared/components/ui/Modal/index.ts
new file mode 100644
index 00000000000..f8109c10da3
--- /dev/null
+++ b/resources/js/shared/components/ui/Modal/index.ts
@@ -0,0 +1 @@
+export * as Modal from "./Modal";
diff --git a/resources/js/shared/components/ui/index.ts b/resources/js/shared/components/ui/index.ts
new file mode 100644
index 00000000000..88362fe2774
--- /dev/null
+++ b/resources/js/shared/components/ui/index.ts
@@ -0,0 +1,3 @@
+export * from "./Button";
+export * from "./IconWrapper";
+export * from "./Modal";
diff --git a/resources/js/shared/hooks/index.ts b/resources/js/shared/hooks/index.ts
new file mode 100644
index 00000000000..2771c524f02
--- /dev/null
+++ b/resources/js/shared/hooks/index.ts
@@ -0,0 +1 @@
+export * from "./useBreakpoint";
diff --git a/resources/js/shared/hooks/useBreakpoint.ts b/resources/js/shared/hooks/useBreakpoint.ts
new file mode 100644
index 00000000000..74e90f37ef0
--- /dev/null
+++ b/resources/js/shared/hooks/useBreakpoint.ts
@@ -0,0 +1,36 @@
+import { useLayoutEffect, useState } from "react";
+import resolveConfig from "tailwindcss/resolveConfig";
+
+// I don't like this 🥀
+import tailwindConfig from "../../../../tailwind.config";
+
+const config = resolveConfig(tailwindConfig);
+
+const screens = config.theme.screens;
+type Breakpoint = keyof typeof screens;
+
+function matches(breakpoint: Breakpoint) {
+ const value = screens[breakpoint] ?? "999999px";
+ const query = window.matchMedia(`(min-width: ${value})`);
+ return query.matches;
+}
+
+export const useBreakpoint = (breakpoint: Breakpoint) => {
+ const [match, setMatch] = useState(matches(breakpoint));
+
+ useLayoutEffect(() => {
+ if (!("matchMedia" in window)) return undefined;
+
+ function track() {
+ const aux = matches(breakpoint);
+ if (aux !== match) {
+ setMatch(aux);
+ }
+ }
+
+ window.addEventListener("resize", track);
+ return () => window.removeEventListener("resize", track);
+ });
+
+ return match;
+};
diff --git a/resources/js/shared/index.ts b/resources/js/shared/index.ts
new file mode 100644
index 00000000000..073109e3ada
--- /dev/null
+++ b/resources/js/shared/index.ts
@@ -0,0 +1,5 @@
+export * from "./assets";
+export * from "./components";
+export * from "./hooks";
+export * from "./sections";
+export * from "./utils";
diff --git a/resources/js/shared/sections/ErrorBoundary/ErrorBoundary.tsx b/resources/js/shared/sections/ErrorBoundary/ErrorBoundary.tsx
new file mode 100644
index 00000000000..5e4aeaed2ee
--- /dev/null
+++ b/resources/js/shared/sections/ErrorBoundary/ErrorBoundary.tsx
@@ -0,0 +1,14 @@
+import React from "react";
+
+import { LightitLogo } from "~/assets";
+
+export const ErrorBoundary = () => {
+ return (
+
+
+
+
+ Something went terribly wrong!
+
+ );
+};
diff --git a/resources/js/shared/sections/ErrorBoundary/index.ts b/resources/js/shared/sections/ErrorBoundary/index.ts
new file mode 100644
index 00000000000..a6ad481e7c0
--- /dev/null
+++ b/resources/js/shared/sections/ErrorBoundary/index.ts
@@ -0,0 +1 @@
+export * from "./ErrorBoundary";
diff --git a/resources/js/shared/sections/Navbar/NavBar.tsx b/resources/js/shared/sections/Navbar/NavBar.tsx
new file mode 100644
index 00000000000..cd5d7f24ac3
--- /dev/null
+++ b/resources/js/shared/sections/Navbar/NavBar.tsx
@@ -0,0 +1,55 @@
+import React from "react";
+
+export interface NavBarProps {
+ setSidebarOpen?: (v: boolean) => void;
+}
+
+export const Navbar = ({ setSidebarOpen }: NavBarProps) => {
+ return (
+
+ {setSidebarOpen && (
+
+ )}
+
+ {/* Separator */}
+
+
+
+
+
+
+
+
+ {/* Separator */}
+
+
+
+
+ );
+};
diff --git a/resources/js/shared/sections/Navbar/index.ts b/resources/js/shared/sections/Navbar/index.ts
new file mode 100644
index 00000000000..578f53ef494
--- /dev/null
+++ b/resources/js/shared/sections/Navbar/index.ts
@@ -0,0 +1 @@
+export * from "./NavBar";
diff --git a/resources/js/shared/sections/NotFound/NotFound.tsx b/resources/js/shared/sections/NotFound/NotFound.tsx
new file mode 100644
index 00000000000..baa71309105
--- /dev/null
+++ b/resources/js/shared/sections/NotFound/NotFound.tsx
@@ -0,0 +1,29 @@
+import React from "react";
+import { useNavigate } from "react-router-dom";
+
+import { LightitLogo } from "~/assets";
+import { Button } from "~/components";
+
+export const NotFound = () => {
+ const navigate = useNavigate();
+ return (
+
+
+
+
+
+
+ Looks like the page you are trying to access does not exist.
+
+
+
+
+ );
+};
diff --git a/resources/js/shared/sections/NotFound/index.ts b/resources/js/shared/sections/NotFound/index.ts
new file mode 100644
index 00000000000..c7283ceae7c
--- /dev/null
+++ b/resources/js/shared/sections/NotFound/index.ts
@@ -0,0 +1 @@
+export * from "./NotFound";
diff --git a/resources/js/shared/sections/Sidebar/Sidebar.tsx b/resources/js/shared/sections/Sidebar/Sidebar.tsx
new file mode 100644
index 00000000000..a84bd9d7554
--- /dev/null
+++ b/resources/js/shared/sections/Sidebar/Sidebar.tsx
@@ -0,0 +1,48 @@
+import React from "react";
+import { Link, useLocation } from "react-router-dom";
+import { twMerge as tw } from "tailwind-merge";
+
+import { ROUTES } from "~/router";
+
+export const Sidebar = () => {
+ const location = useLocation();
+ const current = location.pathname;
+ const navigation = [{ name: "Home", href: ROUTES.home }];
+
+ return (
+ <>
+ {/* Static sidebar for desktop */}
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/resources/js/shared/sections/Sidebar/index.ts b/resources/js/shared/sections/Sidebar/index.ts
new file mode 100644
index 00000000000..d6789989723
--- /dev/null
+++ b/resources/js/shared/sections/Sidebar/index.ts
@@ -0,0 +1 @@
+export * from "./Sidebar";
diff --git a/resources/js/shared/sections/index.ts b/resources/js/shared/sections/index.ts
new file mode 100644
index 00000000000..69813aa26b5
--- /dev/null
+++ b/resources/js/shared/sections/index.ts
@@ -0,0 +1,4 @@
+export * from "./ErrorBoundary";
+export * from "./Navbar";
+export * from "./NotFound";
+export * from "./Sidebar";
diff --git a/resources/js/shared/utils/asyncTimeout.ts b/resources/js/shared/utils/asyncTimeout.ts
new file mode 100644
index 00000000000..c107249e177
--- /dev/null
+++ b/resources/js/shared/utils/asyncTimeout.ts
@@ -0,0 +1,2 @@
+export const asyncTimeout = (ms: number) =>
+ new Promise((resolve) => setTimeout(resolve, ms));
diff --git a/resources/js/shared/utils/dateWithoutTimezone.ts b/resources/js/shared/utils/dateWithoutTimezone.ts
new file mode 100644
index 00000000000..78007eb527a
--- /dev/null
+++ b/resources/js/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/resources/js/shared/utils/forwardRef.ts b/resources/js/shared/utils/forwardRef.ts
new file mode 100644
index 00000000000..4e3722e92cc
--- /dev/null
+++ b/resources/js/shared/utils/forwardRef.ts
@@ -0,0 +1,8 @@
+import React from "react";
+
+// Here we are re-declaring the forwardRef type to support generics being passed to it
+
+// eslint-disable-next-line @typescript-eslint/ban-types
+export const forwardRef = React.forwardRef as (
+ render: (props: P, ref: React.Ref) => React.ReactElement | null,
+) => (props: P & React.RefAttributes) => React.ReactElement | null;
diff --git a/resources/js/shared/utils/index.ts b/resources/js/shared/utils/index.ts
new file mode 100644
index 00000000000..b0554e98b21
--- /dev/null
+++ b/resources/js/shared/utils/index.ts
@@ -0,0 +1,4 @@
+export * from "./asyncTimeout";
+export * from "./dateWithoutTimezone";
+export * from "./forwardRef";
+export * from "./tw";
diff --git a/resources/js/shared/utils/tw.ts b/resources/js/shared/utils/tw.ts
new file mode 100644
index 00000000000..7a8904b243b
--- /dev/null
+++ b/resources/js/shared/utils/tw.ts
@@ -0,0 +1,10 @@
+import { twMerge } from "tailwind-merge";
+
+/**
+ * This is now just a name alias for twMerge, we were using clsx in here too
+ * But as it turns out, twMerge by itself is doing exactly what we need
+ * It just doesn't support the object api part of clsx and that's actually good
+ *
+ * @typeParam T - target string
+ */
+export const tw = twMerge;
diff --git a/resources/js/stores/index.ts b/resources/js/stores/index.ts
index f23be279181..7cd47a256fd 100644
--- a/resources/js/stores/index.ts
+++ b/resources/js/stores/index.ts
@@ -1 +1,3 @@
+export * from "./useAuthStore";
+export * from "./useExampleStore";
export * from "./useUserStore";
diff --git a/resources/js/stores/useAuthStore.ts b/resources/js/stores/useAuthStore.ts
new file mode 100644
index 00000000000..a909a2fc98f
--- /dev/null
+++ b/resources/js/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 interface 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/resources/js/stores/useExampleStore.ts b/resources/js/stores/useExampleStore.ts
new file mode 100644
index 00000000000..a57a5eb7a0b
--- /dev/null
+++ b/resources/js/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 interface 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/resources/js/ui/common/Button.tsx b/resources/js/ui/common/Button.tsx
index 5bd205fc181..866b6afece2 100644
--- a/resources/js/ui/common/Button.tsx
+++ b/resources/js/ui/common/Button.tsx
@@ -1,6 +1,6 @@
import type { ComponentPropsWithoutRef, ForwardedRef } from "react";
-import { forwardRef, tw } from "@/utils";
+import { forwardRef, tw } from "~/utils";
const BUTTON_VARIANT = {
PRIMARY: "primary",
diff --git a/resources/js/ui/common/Icons.tsx b/resources/js/ui/common/Icons.tsx
index 4933faf3553..870cc3ee258 100644
--- a/resources/js/ui/common/Icons.tsx
+++ b/resources/js/ui/common/Icons.tsx
@@ -1,7 +1,7 @@
import * as heroIcons from "@heroicons/react/24/outline";
-import type { SVGProps } from "@/shared.types";
-import { tw } from "@/utils";
+import type { SVGProps } from "~/shared.types";
+import { tw } from "~/utils";
export const icons = {
...heroIcons,
diff --git a/resources/js/ui/common/Modal.tsx b/resources/js/ui/common/Modal.tsx
index dd81790d48f..5926c24364f 100644
--- a/resources/js/ui/common/Modal.tsx
+++ b/resources/js/ui/common/Modal.tsx
@@ -1,7 +1,7 @@
import type { ReactNode } from "react";
import * as Dialog from "@radix-ui/react-dialog";
-import { tw } from "@/utils";
+import { tw } from "~/utils";
import { icons } from "./Icons";
interface ModalProps {
diff --git a/resources/js/ui/common/Toast/ToastMessage.tsx b/resources/js/ui/common/Toast/ToastMessage.tsx
index 2834fba8a4d..5c53f104f62 100644
--- a/resources/js/ui/common/Toast/ToastMessage.tsx
+++ b/resources/js/ui/common/Toast/ToastMessage.tsx
@@ -1,6 +1,6 @@
-import { XMarkIcon } from "@heroicons/react/20/solid";
+import { XMarkIcon } from "@heroicons/react/24/outline";
-import { tw } from "@/utils";
+import { tw } from "~/utils";
import toastIcons from "./toastIcons";
import type { Toast } from "./toastStore";
diff --git a/resources/js/ui/common/Toast/toastStore.ts b/resources/js/ui/common/Toast/toastStore.ts
index 63c2356f910..1244b06a54e 100644
--- a/resources/js/ui/common/Toast/toastStore.ts
+++ b/resources/js/ui/common/Toast/toastStore.ts
@@ -2,7 +2,7 @@ import type { ReactNode } from "react";
import { v4 as uuid } from "uuid";
import { create } from "zustand";
-import { asyncTimeout } from "@/utils/asyncTimeout";
+import { asyncTimeout } from "~/utils/asyncTimeout";
export const toastTypes = ["info", "success", "error", "warning"] as const;
diff --git a/resources/js/ui/form/Input.tsx b/resources/js/ui/form/Input.tsx
index e842e88307c..dcf264d6fbb 100644
--- a/resources/js/ui/form/Input.tsx
+++ b/resources/js/ui/form/Input.tsx
@@ -1,6 +1,6 @@
import type { ComponentPropsWithoutRef, ForwardedRef, ReactNode } from "react";
-import { forwardRef, tw } from "@/utils";
+import { forwardRef, tw } from "~/utils";
import { IconWrapper } from "../common";
import { Label } from "./Label";
import { Message } from "./Message";
diff --git a/resources/js/ui/form/Label.tsx b/resources/js/ui/form/Label.tsx
index 6fece441321..3fb829ac84a 100644
--- a/resources/js/ui/form/Label.tsx
+++ b/resources/js/ui/form/Label.tsx
@@ -1,6 +1,6 @@
import type { ComponentPropsWithoutRef, FC, ReactNode } from "react";
-import { tw } from "@/utils";
+import { tw } from "~/utils";
export interface LabelProps extends ComponentPropsWithoutRef<"label"> {
label: ReactNode;
diff --git a/resources/js/ui/form/Message.tsx b/resources/js/ui/form/Message.tsx
index 4d53f69ffff..8bfc0c12364 100644
--- a/resources/js/ui/form/Message.tsx
+++ b/resources/js/ui/form/Message.tsx
@@ -1,6 +1,6 @@
import type { ComponentPropsWithoutRef } from "react";
-import { tw } from "@/utils";
+import { tw } from "~/utils";
export interface MessageProps extends ComponentPropsWithoutRef<"p"> {
message?: string;
@@ -16,6 +16,6 @@ export const Message = ({ message, error, className }: MessageProps) => (
!!error && "text-red-400",
)}
>
- {error === true ? "\u200b" : !error ? message ?? "\u200b" : error}
+ {error === true ? "\u200b" : !error ? (message ?? "\u200b") : error}
);
diff --git a/resources/js/vite-env.d.ts b/resources/js/vite-env.d.ts
new file mode 100644
index 00000000000..11f02fe2a00
--- /dev/null
+++ b/resources/js/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php
index b367e5bba29..2a05b6c2d19 100644
--- a/resources/views/app.blade.php
+++ b/resources/views/app.blade.php
@@ -19,7 +19,7 @@
-
+
@viteReactRefresh @vite('resources/js/App.tsx')
diff --git a/routes/api.php b/routes/api.php
index 90da7a4272f..792118d5cad 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -31,7 +31,7 @@
->middleware([])
->group(static function () {
Route::get('/', ListUserController::class);
- Route::get('/{user}', GetUserController::class);
+ Route::get('/{user}', GetUserController::class)->withTrashed();
Route::post('/', StoreUserController::class);
Route::delete('/{user}', DeleteUserController::class);
});
diff --git a/src/Backoffice/Users/App/Controllers/ListUserController.php b/src/Backoffice/Users/App/Controllers/ListUserController.php
index 4a2b7640ae6..f81f471881f 100644
--- a/src/Backoffice/Users/App/Controllers/ListUserController.php
+++ b/src/Backoffice/Users/App/Controllers/ListUserController.php
@@ -7,15 +7,15 @@
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Lightit\Backoffice\Users\App\Transformers\UserTransformer;
-use Lightit\Backoffice\Users\Domain\Models\User;
-use Spatie\QueryBuilder\QueryBuilder;
+use Lightit\Backoffice\Users\Domain\Actions\ListUserAction;
class ListUserController
{
- public function __invoke(Request $request): JsonResponse
- {
- $users = QueryBuilder::for(User::class)
- ->get();
+ public function __invoke(
+ Request $request,
+ ListUserAction $action,
+ ): JsonResponse {
+ $users = $action->execute();
return responder()
->success($users, UserTransformer::class)
diff --git a/src/Backoffice/Users/App/Request/StoreUserRequest.php b/src/Backoffice/Users/App/Request/StoreUserRequest.php
index ec7e99199b8..e6e1563860b 100644
--- a/src/Backoffice/Users/App/Request/StoreUserRequest.php
+++ b/src/Backoffice/Users/App/Request/StoreUserRequest.php
@@ -11,7 +11,9 @@
class StoreUserRequest extends FormRequest
{
public const NAME = 'name';
+
public const EMAIL = 'email';
+
public const PASSWORD = 'password';
/**
diff --git a/src/Backoffice/Users/App/Transformers/UserTransformer.php b/src/Backoffice/Users/App/Transformers/UserTransformer.php
index 5da089b0e89..80e0b9739ba 100644
--- a/src/Backoffice/Users/App/Transformers/UserTransformer.php
+++ b/src/Backoffice/Users/App/Transformers/UserTransformer.php
@@ -15,9 +15,9 @@ class UserTransformer extends Transformer
public function transform(User $user): array
{
return [
- 'id' => (int) $user->id,
- 'name' => (string) $user->name,
- 'email' => (string) $user->email
+ 'id' => $user->id,
+ 'name' => $user->name,
+ 'email' => $user->email,
];
}
}
diff --git a/src/Backoffice/Users/Domain/Actions/ListUserAction.php b/src/Backoffice/Users/Domain/Actions/ListUserAction.php
new file mode 100644
index 00000000000..3c2456045eb
--- /dev/null
+++ b/src/Backoffice/Users/Domain/Actions/ListUserAction.php
@@ -0,0 +1,24 @@
+
+ */
+ public function execute(): Collection
+ {
+ return QueryBuilder::for(User::class)
+ ->allowedFilters(['email'])
+ ->allowedSorts('email')
+ ->get();
+ }
+}
diff --git a/src/Shared/App/Console/Commands/TestCommand.php b/src/Shared/App/Console/Commands/TestCommand.php
index 9771407a14b..9e4360cdbb2 100644
--- a/src/Shared/App/Console/Commands/TestCommand.php
+++ b/src/Shared/App/Console/Commands/TestCommand.php
@@ -21,8 +21,8 @@ class TestCommand extends Command
public function handle(LoggerInterface $logger): int
{
- $logger->info("Hi, Im am Logger! How are u?");
- $this->info("Done");
+ $logger->info('Hi, Im am Logger! How are u?');
+ $this->info('Done');
return Command::SUCCESS;
}
diff --git a/src/Shared/App/Exceptions/ExceptionHandler.php b/src/Shared/App/Exceptions/ExceptionHandler.php
index ebfac1298f4..c1f718a1a7a 100644
--- a/src/Shared/App/Exceptions/ExceptionHandler.php
+++ b/src/Shared/App/Exceptions/ExceptionHandler.php
@@ -23,6 +23,7 @@
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Sentry\Laravel\Integration;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class ExceptionHandler
@@ -35,6 +36,7 @@ protected function convertDefaultException(Exception $exception): void
$this->convert($exception, array_diff_key([
AuthenticationException::class => UnauthenticatedException::class,
AuthorizationException::class => UnauthorizedException::class,
+ AccessDeniedHttpException::class => UnauthorizedException::class,
NotFoundHttpException::class => PageNotFoundException::class,
ModelNotFoundException::class => ModelNotFoundHttpException::class,
BaseRelationNotFoundException::class => RelationNotFoundException::class,
@@ -76,6 +78,7 @@ protected function convert(Exception $exception, array $convert): void
if (is_callable($target)) {
$target($exception);
}
+
throw new $target();
}
}
diff --git a/src/Shared/App/Providers/AppServiceProvider.php b/src/Shared/App/Providers/AppServiceProvider.php
index e9c1663721b..24d0a291727 100644
--- a/src/Shared/App/Providers/AppServiceProvider.php
+++ b/src/Shared/App/Providers/AppServiceProvider.php
@@ -4,6 +4,7 @@
namespace Lightit\Shared\App\Providers;
+use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
@@ -20,5 +21,6 @@ public function register(): void
*/
public function boot(): void
{
+ DB::prohibitDestructiveCommands($this->app->isProduction());
}
}
diff --git a/src/Shared/App/Providers/EventServiceProvider.php b/src/Shared/App/Providers/EventServiceProvider.php
index 6868ba57c87..b40c40e167e 100644
--- a/src/Shared/App/Providers/EventServiceProvider.php
+++ b/src/Shared/App/Providers/EventServiceProvider.php
@@ -22,8 +22,8 @@ class EventServiceProvider extends ServiceProvider
SendEmailVerificationNotification::class,
],
TestEvent::class => [
- TestListener::class
- ]
+ TestListener::class,
+ ],
];
/**
@@ -33,6 +33,10 @@ public function boot(): void
{
}
+ public function register(): void
+ {
+ }
+
/**
* Determine if events and listeners should be automatically discovered.
*/
diff --git a/stubs/model.stub b/stubs/model.stub
index 729e12e9cfc..63602790e29 100644
--- a/stubs/model.stub
+++ b/stubs/model.stub
@@ -9,5 +9,4 @@ use Illuminate\Database\Eloquent\Model;
class {{ class }} extends Model
{
- use HasFactory;
}
diff --git a/tailwind.config.js b/tailwind.config.js
deleted file mode 100644
index 4022865cddc..00000000000
--- a/tailwind.config.js
+++ /dev/null
@@ -1,10 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-module.exports = {
- content: ["./resources/**/*.{js,ts,jsx,tsx}"],
- plugins: [require("@tailwindcss/typography"), require("tailwindcss-animate")],
- theme: {
- fontFamily: {
- sans: ["Inter", "sans-serif"],
- },
- },
-};
diff --git a/tailwind.config.ts b/tailwind.config.ts
new file mode 100644
index 00000000000..eaaf3e963c6
--- /dev/null
+++ b/tailwind.config.ts
@@ -0,0 +1,39 @@
+import forms from "@tailwindcss/forms";
+import typography from "@tailwindcss/typography";
+import type { Config } from "tailwindcss";
+import animate from "tailwindcss-animate";
+
+export default {
+ content: ["./resources/**/*.{js,ts,jsx,tsx}"],
+ theme: {
+ screens: {
+ // These are the default media queries.
+ // We're declaring them to make it easier to import and use in react for js checks
+ sm: "640px",
+ md: "768px",
+ lg: "1024px",
+ xl: "1280px",
+ "2xl": "1536px",
+ },
+ extend: {
+ keyframes: {
+ "accordion-down": {
+ from: { height: "0" },
+ to: { height: "var(--radix-accordion-content-height)" },
+ },
+ "accordion-up": {
+ from: { height: "var(--radix-accordion-content-height)" },
+ to: { height: "0" },
+ },
+ },
+ animation: {
+ "accordion-down": "accordion-down 0.2s ease-out",
+ "accordion-up": "accordion-up 0.2s ease-out",
+ },
+ fontFamily: {
+ sans: ["Inter", "sans-serif"],
+ },
+ },
+ },
+ plugins: [typography, forms, animate],
+} satisfies Config;
diff --git a/templates/domains/api/example.ts.hbs b/templates/domains/api/example.ts.hbs
new file mode 100644
index 00000000000..9535847cef0
--- /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
new file mode 100644
index 00000000000..3e55cd5815a
--- /dev/null
+++ b/templates/domains/api/index.ts.hbs
@@ -0,0 +1,2 @@
+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 00000000000..b9f26fc401c
--- /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 00000000000..263716e9424
--- /dev/null
+++ b/templates/domains/api/schemas/index.hbs
@@ -0,0 +1 @@
+export * from "./{{camelCase name}}Schemas";
diff --git a/templates/domains/components/component-example.tsx.hbs b/templates/domains/components/component-example.tsx.hbs
new file mode 100644
index 00000000000..3952e1490b2
--- /dev/null
+++ b/templates/domains/components/component-example.tsx.hbs
@@ -0,0 +1,5 @@
+import * as React from "react"
+
+export const ComponentExample: React.FC = () => {
+ return This is an Component Example.
+}
diff --git a/templates/domains/components/index.ts.hbs b/templates/domains/components/index.ts.hbs
new file mode 100644
index 00000000000..70d14c526ee
--- /dev/null
+++ b/templates/domains/components/index.ts.hbs
@@ -0,0 +1 @@
+export * from "./ExampleComponent";
diff --git a/templates/domains/context/context.hbs b/templates/domains/context/context.hbs
new file mode 100644
index 00000000000..8c911fe3180
--- /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
new file mode 100644
index 00000000000..6f16fdcf9cd
--- /dev/null
+++ b/templates/domains/context/index.ts.hbs
@@ -0,0 +1 @@
+export * from "./use{{pascalCase name}}Context";
diff --git a/templates/domains/domain-barrel.ts.hbs b/templates/domains/domain-barrel.ts.hbs
new file mode 100644
index 00000000000..03f8044166f
--- /dev/null
+++ b/templates/domains/domain-barrel.ts.hbs
@@ -0,0 +1 @@
+export * from "./{{pascalCase name}}Router.tsx";
diff --git a/templates/domains/queries/index.ts.hbs b/templates/domains/queries/index.ts.hbs
new file mode 100644
index 00000000000..35793da6d26
--- /dev/null
+++ b/templates/domains/queries/index.ts.hbs
@@ -0,0 +1 @@
+export * from "./{{kebabCase name}}";
diff --git a/templates/domains/queries/query-example.tsx.hbs b/templates/domains/queries/query-example.tsx.hbs
new file mode 100644
index 00000000000..7344a7f115b
--- /dev/null
+++ b/templates/domains/queries/query-example.tsx.hbs
@@ -0,0 +1,144 @@
+import { createQueryKeys } from "@lukemorales/query-key-factory";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+
+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 00000000000..497f1ac41e3
--- /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
new file mode 100644
index 00000000000..64e59345d8d
--- /dev/null
+++ b/templates/domains/screens/index.ts.hbs
@@ -0,0 +1 @@
+export * from "./ScreenExample";
diff --git a/templates/domains/screens/screen-example.tsx.hbs b/templates/domains/screens/screen-example.tsx.hbs
new file mode 100644
index 00000000000..b643a2c5ba4
--- /dev/null
+++ b/templates/domains/screens/screen-example.tsx.hbs
@@ -0,0 +1,5 @@
+import * as React from "react"
+
+export const ScreenExample = () => {
+ return This is a ScreenExample.
+}
diff --git a/templates/domains/sections/index.ts.hbs b/templates/domains/sections/index.ts.hbs
new file mode 100644
index 00000000000..0e4c85890fc
--- /dev/null
+++ b/templates/domains/sections/index.ts.hbs
@@ -0,0 +1 @@
+export * from "./SectionExample";
diff --git a/templates/domains/sections/section-example.tsx.hbs b/templates/domains/sections/section-example.tsx.hbs
new file mode 100644
index 00000000000..71fbd1b6aad
--- /dev/null
+++ b/templates/domains/sections/section-example.tsx.hbs
@@ -0,0 +1,5 @@
+import * as React from "react"
+
+export const SectionExample = () => {
+ return This is an Section Example.
+}
diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php
index f8e6880eaf4..add5502a6da 100644
--- a/tests/Feature/ExampleTest.php
+++ b/tests/Feature/ExampleTest.php
@@ -4,20 +4,14 @@
namespace Tests\Feature;
-// use Illuminate\Foundation\Testing\RefreshDatabase;
-use Tests\TestCase;
+use function Pest\Laravel\get;
-class ExampleTest extends TestCase
-{
- /**
- * A basic test example.
- */
- public function test_the_application_returns_a_successful_response(): void
- {
- $this->withoutVite();
+it('returns a successful response', function () {
+ // Example of Ignore PhpStanLine
+ /** @phpstan-ignore-next-line */
+ test()->withoutVite();
- $response = $this->get('/');
+ $response = get('/');
- $response->assertStatus(200);
- }
-}
+ $response->assertSuccessful();
+});
diff --git a/tests/Feature/Users/ListUserTest.php b/tests/Feature/Users/ListUserTest.php
index 8edcae572b6..6b77689dc2e 100644
--- a/tests/Feature/Users/ListUserTest.php
+++ b/tests/Feature/Users/ListUserTest.php
@@ -5,27 +5,30 @@
namespace Tests\Feature\Users;
use Database\Factories\UserFactory;
-use Illuminate\Support\Collection;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Testing\Fluent\AssertableJson;
use Lightit\Backoffice\Users\App\Transformers\UserTransformer;
-use Lightit\Backoffice\Users\Domain\Models\User;
use function Pest\Laravel\getJson;
describe('users', function () {
/** @see StoreUserController */
it('can list users successfully', function () {
- /** @var Collection $users */
$users = UserFactory::new()
- ->count(5)
- ->create();
-
- $transformer = new UserTransformer();
+ ->createMany(5);
getJson(url('/api/users'))
->assertSuccessful()
- ->assertExactJson([
- 'status' => 200,
- 'success' => true,
- 'data' => $users->map(fn(User $user) => $transformer->transform($user))->toArray(),
- ]);
+ ->assertJson(
+ fn (AssertableJson $json) =>
+ $json->where('status', JsonResponse::HTTP_OK)
+ ->where('success', true)
+ ->has(
+ 'data',
+ fn (AssertableJson $json) =>
+ $json->whereAll(
+ transformation($users, UserTransformer::class)->transform() ?? []
+ )
+ )
+ );
});
});
diff --git a/tests/Feature/Users/StoreUserTest.php b/tests/Feature/Users/StoreUserTest.php
index 59c3485b106..176ac057dfb 100644
--- a/tests/Feature/Users/StoreUserTest.php
+++ b/tests/Feature/Users/StoreUserTest.php
@@ -20,7 +20,7 @@
Notification::fake();
$data = StoreUserRequestFactory::new()->create([
- 'password' => 'passw0rd'
+ 'password' => 'passw0rd',
]);
postJson(url('/api/users'), $data)
@@ -28,7 +28,7 @@
assertDatabaseHas('users', [
'name' => $data['name'],
- 'email' => $data['email']
+ 'email' => $data['email'],
]);
$user = User::query()->firstOrFail();
diff --git a/tests/RequestFactories/StoreUserRequestFactory.php b/tests/RequestFactories/StoreUserRequestFactory.php
index 8249b7ffb47..1c889dedfe1 100644
--- a/tests/RequestFactories/StoreUserRequestFactory.php
+++ b/tests/RequestFactories/StoreUserRequestFactory.php
@@ -14,7 +14,7 @@ public function definition(): array
'name' => $this->faker->name,
'email' => $this->faker->email,
'password' => 'passw0rd',
- 'password_confirmation' => 'passw0rd'
+ 'password_confirmation' => 'passw0rd',
];
}
}
diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php
index 262903d1487..e27b4c04758 100644
--- a/tests/Unit/ExampleTest.php
+++ b/tests/Unit/ExampleTest.php
@@ -4,15 +4,6 @@
namespace Tests\Unit;
-use PHPUnit\Framework\TestCase;
-
-class ExampleTest extends TestCase
-{
- /**
- * A basic test example.
- */
- public function test_that_true_is_true(): void
- {
- $this->assertTrue(true);
- }
-}
+it('asserts that true is true', function () {
+ expect(true)->toBeTrue();
+});
diff --git a/tsconfig.json b/tsconfig.json
index 5ca221eea5a..38f06de0fc7 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,27 +1,36 @@
{
"compilerOptions": {
- "allowJs": true,
- "baseUrl": "./resources/js",
- "esModuleInterop": true,
- "forceConsistentCasingInFileNames": true,
- "incremental": true,
- "isolatedModules": true,
- "jsx": "react-jsx",
- "lib": ["dom", "dom.iterable", "esnext"],
+ "rootDir": "./",
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["esnext", "DOM", "DOM.Iterable"],
"module": "esnext",
- "moduleResolution": "node",
- "noEmit": true,
- "noUncheckedIndexedAccess": true,
+ "skipLibCheck": true,
+ "baseUrl": "./resources/js",
"paths": {
- "@/*": ["*"]
+ "~/assets": ["./shared/assets"],
+ "~/components": ["./shared/components/ui"],
+ "~/hooks": ["./shared/hooks"],
+ "~/icons": ["./shared/components/icons"],
+ "~/sections": ["./shared/sections"],
+ "~/utils": ["./shared/utils"],
+ "~/*": ["*"]
},
+
+ /* Bundler mode */
+ "moduleResolution": "node",
+ "allowImportingTsExtensions": true,
+ "allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
- "skipLibCheck": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "preserve",
+
+ /* Linting */
"strict": true,
- "target": "ESNext",
- "types": ["vite/client"],
"noUnusedLocals": true,
- "noUnusedParameters": true
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
},
"include": ["./resources/js/**/*", ".eslintrc.cjs", "prettier.config.cjs"]
}
diff --git a/vite.config.js b/vite.config.js
index 8c63d4f1b6f..02d0d1a29e6 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,3 +1,4 @@
+import path from "path";
import { sentryVitePlugin } from "@sentry/vite-plugin";
/* eslint-disable import/no-extraneous-dependencies */
import react from "@vitejs/plugin-react";
@@ -25,10 +26,38 @@ export default ({ mode }) => {
name: "blade",
},
],
-
build: {
sourcemap: true,
},
+ resolve: {
+ alias: [
+ {
+ find: "~/assets",
+ replacement: path.resolve(__dirname, "./resources/js/shared/assets"),
+ },
+ {
+ find: "~/components",
+ replacement: path.resolve(__dirname, "./resources/js/shared/components/ui"),
+ },
+ {
+ find: "~/hooks",
+ replacement: path.resolve(__dirname, "./resources/js/shared/hooks"),
+ },
+ {
+ find: "~/icons",
+ replacement: path.resolve(__dirname, "./resources/js/shared/components/icons"),
+ },
+ {
+ find: "~/sections",
+ replacement: path.resolve(__dirname, "./resources/js/shared/sections"),
+ },
+ {
+ find: "~/utils",
+ replacement: path.resolve(__dirname, "./resources/js/shared/utils"),
+ },
+ { find: "~", replacement: path.resolve(__dirname, "./resources/js") },
+ ],
+ },
};
const env = process.env;
@@ -37,7 +66,7 @@ export default ({ mode }) => {
const sentryAuthToken = env.VITE_SENTRY_AUTH_TOKEN;
const sentryOrg = env.VITE_SENTRY_ORGANIZATION;
const sentryProject = env.VITE_SENTRY_PROJECT;
-
+
if (sentryAuthToken && sentryOrg && sentryProject) {
config.plugins.push(sentryVitePlugin({
authToken: sentryAuthToken,