fixing so much trouble

This commit is contained in:
Maulana Sodiqin
2025-05-23 00:28:29 +07:00
parent 376e646c10
commit 9a0fee65a5
61 changed files with 735 additions and 362 deletions
@@ -18,7 +18,7 @@ import {
RowSelectionState, RowSelectionState,
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import ModalEditAccount from './_components/modal-edit-account'; import ModalEditAccount from './_components/modal-edit-account';
import { useQueryState } from '../../hook/use-query-state'; import { useQueryState } from '@imphnen-frontend-service/utils';
interface Account { interface Account {
id: number; id: number;
@@ -166,7 +166,7 @@ export const Components: FC = (): ReactElement => {
</Button> </Button>
{showFilter && ( {showFilter && (
<div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg"> <div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg">
<Filter onClose={() => setShowFilter(false)} /> <Filter onClose={() => setShowFilter(false)} options={[]} />
</div> </div>
)} )}
</div> </div>
@@ -10,7 +10,7 @@ import { FC, Fragment, ReactElement, useState } from 'react';
import ModalAddItem from './_components/modal-add-item'; import ModalAddItem from './_components/modal-add-item';
import ModalEditItem from './_components/modal-edit-item'; import ModalEditItem from './_components/modal-edit-item';
import ModalDeleteItem from './_components/modal-delete-item'; import ModalDeleteItem from './_components/modal-delete-item';
import { useQueryState } from '../../hook/use-query-state'; import { useQueryState } from '@imphnen-frontend-service/utils';
export const Components: FC = (): ReactElement => { export const Components: FC = (): ReactElement => {
const [showModalAddItem, setShowModalAddItem] = useState(false); const [showModalAddItem, setShowModalAddItem] = useState(false);
@@ -2,24 +2,19 @@ import { useForm } from 'react-hook-form';
import { import {
authLoginSchema, authLoginSchema,
TLoginRequest, TLoginRequest,
usePostLogin,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { toast } from 'sonner'; import { useSession } from '@imphnen-frontend-service/utils';
export const useLogin = () => { export const useLogin = () => {
const postLogin = usePostLogin();
const form = useForm<TLoginRequest>({ const form = useForm<TLoginRequest>({
resolver: zodResolver(authLoginSchema), resolver: zodResolver(authLoginSchema),
mode: 'all', mode: 'all',
}); });
const onSubmit = form.handleSubmit((data) => { const { signIn } = useSession();
postLogin.mutate(data, {
onSuccess: () => toast.success("Login sukses"), const onSubmit = form.handleSubmit((data) => signIn(data));
onError: (error) => toast.error(error.message),
});
});
return { return {
form, form,
@@ -32,7 +32,16 @@ export const Components: FC = (): ReactElement => {
size="lg" size="lg"
className="w-full" className="w-full"
/> />
<Button type="submit" size="md" className="w-full"> <Button
disabled={
form.formState.isSubmitting ||
form.formState.isValidating ||
!form.formState.isValid
}
type="submit"
size="md"
className="w-full"
>
Login Login
</Button> </Button>
</form> </form>
@@ -1,18 +0,0 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
export const AppLayout: FC = (): ReactElement => {
return (
<div className="bg-primary-50 min-h-screen flex justify-center">
<div className="bg-primary-50 min-h-screen w-full flex">
<BackofficeSidebar />
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
<Outlet />
</div>
</div>
</div>
);
};
export default AppLayout;
@@ -1,18 +0,0 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
export const AppLayout: FC = (): ReactElement => {
return (
<div className="bg-primary-50 min-h-screen flex justify-center">
<div className="bg-primary-50 min-h-screen w-full flex">
<BackofficeSidebar />
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
<Outlet />
</div>
</div>
</div>
);
};
export default AppLayout;
@@ -1,18 +0,0 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
export const AppLayout: FC = (): ReactElement => {
return (
<div className="bg-primary-50 min-h-screen flex justify-center">
<div className="bg-primary-50 min-h-screen w-full flex">
<BackofficeSidebar />
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
<Outlet />
</div>
</div>
</div>
);
};
export default AppLayout;
-18
View File
@@ -1,18 +0,0 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
export const AppLayout: FC = (): ReactElement => {
return (
<div className="bg-primary-50 min-h-screen flex justify-center">
<div className="bg-primary-50 min-h-screen w-full flex">
<BackofficeSidebar />
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
<Outlet />
</div>
</div>
</div>
);
};
export default AppLayout;
-18
View File
@@ -1,18 +0,0 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
export const AppLayout: FC = (): ReactElement => {
return (
<div className="bg-primary-50 min-h-screen flex justify-center">
<div className="bg-primary-50 min-h-screen w-full flex">
<BackofficeSidebar />
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
<Outlet />
</div>
</div>
</div>
);
};
export default AppLayout;
@@ -1,18 +0,0 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
export const AppLayout: FC = (): ReactElement => {
return (
<div className="bg-primary-50 min-h-screen flex justify-center">
<div className="bg-primary-50 min-h-screen w-full flex">
<BackofficeSidebar />
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
<Outlet />
</div>
</div>
</div>
);
};
export default AppLayout;
+100 -3
View File
@@ -1,11 +1,108 @@
import { LoaderFunctionArgs } from 'react-router-dom'; import {
PERMISSIONS,
SessionToken,
SessionUser,
} from '@imphnen-frontend-service/utils';
import { LoaderFunctionArgs, redirect } from 'react-router';
const mappingPublicRoutes = [
'/auth/login',
'/auth/forgot',
'/auth/new-password',
];
const mappingRoutePermissions = [
{
path: '/dashboard',
permissions: [],
},
{
path: '/users',
permissions: [PERMISSIONS.USERS.READ_LIST],
},
{
path: '/users/create',
permissions: [PERMISSIONS.USERS.CREATE],
},
{
path: '/users/update',
permissions: [PERMISSIONS.USERS.UPDATE],
},
{
path: '/users/detail',
permissions: [PERMISSIONS.USERS.READ_DETAIL],
},
{
path: '/roles',
permissions: [PERMISSIONS.ROLES.READ_LIST],
},
{
path: '/roles/create',
permissions: [PERMISSIONS.ROLES.CREATE],
},
{
path: '/roles/update',
permissions: [PERMISSIONS.ROLES.UPDATE],
},
{
path: '/roles/detail',
permissions: [PERMISSIONS.ROLES.READ_DETAIL],
},
{
path: '/permissions',
permissions: [PERMISSIONS.PERMISSIONS.READ_LIST],
},
{
path: '/permissions/create',
permissions: [PERMISSIONS.PERMISSIONS.CREATE],
},
{
path: '/permissions/update',
permissions: [PERMISSIONS.PERMISSIONS.UPDATE],
},
{
path: '/permissions/detail',
permissions: [PERMISSIONS.PERMISSIONS.READ_DETAIL],
},
];
//TODO : Fix this later
// const redirectToFirstAccessibleRoute = (userPermissions: string[]) => {
// const fallback = mappingRoutePermissions.find((route) =>
// route.permissions.some((perm) => userPermissions.includes(perm))
// );
// return redirect(fallback?.path ?? '/auth/login');
// };
export const middleware = async ({ request }: LoaderFunctionArgs) => { export const middleware = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url); const url = new URL(request.url);
const pathname = url.pathname; const pathname = url.pathname;
const session = SessionUser.get();
const session_token = SessionToken.get();
const token = session_token?.token?.access_token;
const userPermissions =
session?.role?.permissions?.map?.((perm) => perm?.name) ?? [];
if (pathname) return null; if (mappingPublicRoutes.includes(pathname)) {
if (token) return redirect('/dashboard');
return null;
}
if (!session) return redirect('/auth/login');
const matchedRoute = mappingRoutePermissions.find(
(route) => route.path === pathname
);
if (matchedRoute) {
const hasPermission =
!matchedRoute.permissions ||
matchedRoute.permissions.some((perm) => userPermissions.includes(perm));
if (!hasPermission) {
return '/dashboard';
}
}
return null; return null;
}; };
+4 -9
View File
@@ -2,24 +2,19 @@ import { useForm } from 'react-hook-form';
import { import {
authLoginSchema, authLoginSchema,
TLoginRequest, TLoginRequest,
usePostLogin,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { toast } from 'sonner'; import { useSession } from '@imphnen-frontend-service/utils';
export const useLogin = () => { export const useLogin = () => {
const postLogin = usePostLogin();
const form = useForm<TLoginRequest>({ const form = useForm<TLoginRequest>({
resolver: zodResolver(authLoginSchema), resolver: zodResolver(authLoginSchema),
mode: 'all', mode: 'all',
}); });
const onSubmit = form.handleSubmit((data) => { const { signIn } = useSession();
postLogin.mutate(data, {
onSuccess: () => toast.success("Login sukses"), const onSubmit = form.handleSubmit((data) => signIn(data));
onError: (error) => toast.error(error.message),
});
});
return { return {
form, form,
+101 -3
View File
@@ -1,11 +1,109 @@
import { LoaderFunctionArgs } from 'react-router-dom'; import {
PERMISSIONS,
SessionToken,
SessionUser,
} from '@imphnen-frontend-service/utils';
import { LoaderFunctionArgs, redirect } from 'react-router';
const mappingPublicRoutes = [
'/',
'/auth/login',
'/auth/forgot',
'/auth/new-password',
];
const mappingRoutePermissions = [
{
path: '/dashboard',
permissions: [],
},
{
path: '/users',
permissions: [PERMISSIONS.USERS.READ_LIST],
},
{
path: '/users/create',
permissions: [PERMISSIONS.USERS.CREATE],
},
{
path: '/users/update',
permissions: [PERMISSIONS.USERS.UPDATE],
},
{
path: '/users/detail',
permissions: [PERMISSIONS.USERS.READ_DETAIL],
},
{
path: '/roles',
permissions: [PERMISSIONS.ROLES.READ_LIST],
},
{
path: '/roles/create',
permissions: [PERMISSIONS.ROLES.CREATE],
},
{
path: '/roles/update',
permissions: [PERMISSIONS.ROLES.UPDATE],
},
{
path: '/roles/detail',
permissions: [PERMISSIONS.ROLES.READ_DETAIL],
},
{
path: '/permissions',
permissions: [PERMISSIONS.PERMISSIONS.READ_LIST],
},
{
path: '/permissions/create',
permissions: [PERMISSIONS.PERMISSIONS.CREATE],
},
{
path: '/permissions/update',
permissions: [PERMISSIONS.PERMISSIONS.UPDATE],
},
{
path: '/permissions/detail',
permissions: [PERMISSIONS.PERMISSIONS.READ_DETAIL],
},
];
//TODO : Fix this later
// const redirectToFirstAccessibleRoute = (userPermissions: string[]) => {
// const fallback = mappingRoutePermissions.find((route) =>
// route.permissions.some((perm) => userPermissions.includes(perm))
// );
// return redirect(fallback?.path ?? '/auth/login');
// };
export const middleware = async ({ request }: LoaderFunctionArgs) => { export const middleware = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url); const url = new URL(request.url);
const pathname = url.pathname; const pathname = url.pathname;
const session = SessionUser.get();
const session_token = SessionToken.get();
const token = session_token?.token?.access_token;
const userPermissions =
session?.role?.permissions?.map?.((perm) => perm?.name) ?? [];
if (pathname) return null; if (mappingPublicRoutes.includes(pathname)) {
if (token) return redirect('/dashboard');
return null;
}
if (!session) return redirect('/auth/login');
const matchedRoute = mappingRoutePermissions.find(
(route) => route.path === pathname
);
if (matchedRoute) {
const hasPermission =
!matchedRoute.permissions ||
matchedRoute.permissions.some((perm) => userPermissions.includes(perm));
if (!hasPermission) {
return '/dashboard';
}
}
return null; return null;
}; };
+4 -7
View File
@@ -2,22 +2,19 @@ import { useForm } from 'react-hook-form';
import { import {
authLoginSchema, authLoginSchema,
TLoginRequest, TLoginRequest,
usePostLogin,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useSession } from '@imphnen-frontend-service/utils';
export const useLogin = () => { export const useLogin = () => {
const postLogin = usePostLogin();
const form = useForm<TLoginRequest>({ const form = useForm<TLoginRequest>({
resolver: zodResolver(authLoginSchema), resolver: zodResolver(authLoginSchema),
mode: 'all', mode: 'all',
}); });
const onSubmit = form.handleSubmit((data) => { const { signIn } = useSession();
postLogin.mutate(data, {
onSuccess: () => console.log('Success Login'), const onSubmit = form.handleSubmit((data) => signIn(data));
});
});
return { return {
form, form,
+1 -24
View File
@@ -1,14 +1,11 @@
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { import {
authRegisterSchema, authRegisterSchema,
stepOneRegisterSchema,
TRegisterRequest, TRegisterRequest,
usePostRegister, usePostRegister,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useEffect, useState } from 'react';
export const useRegister = () => { export const useRegister = () => {
const postRegister = usePostRegister(); const postRegister = usePostRegister();
@@ -21,31 +18,12 @@ export const useRegister = () => {
password: '', password: '',
confirm_password: '', confirm_password: '',
phone_number: '', phone_number: '',
referral_code: '',
referred_by: '',
student_type: '',
}, },
}); });
const [stepValid, setStepValid] = useState(false);
useEffect(() => {
const subscription = form.watch(() => {
const { fullname, email, password, confirm_password } = form.getValues();
const valid = stepOneRegisterSchema.safeParse({
fullname,
email,
password,
confirm_password,
}).success;
setStepValid(valid);
});
return () => subscription.unsubscribe();
}, [form]);
const onSubmit = form.handleSubmit((data) => { const onSubmit = form.handleSubmit((data) => {
postRegister.mutate(data, { postRegister.mutate(data, {
onSuccess: (data) => toast.success(data.message), onSuccess: () => toast.success('Registrasi sukses'),
onError: (error) => toast.error(error.message), onError: (error) => toast.error(error.message),
}); });
}); });
@@ -53,6 +31,5 @@ export const useRegister = () => {
return { return {
form, form,
onSubmit, onSubmit,
isStepOneValid: stepValid,
}; };
}; };
+101 -3
View File
@@ -1,11 +1,109 @@
import { LoaderFunctionArgs } from 'react-router-dom'; import {
PERMISSIONS,
SessionToken,
SessionUser,
} from '@imphnen-frontend-service/utils';
import { LoaderFunctionArgs, redirect } from 'react-router';
const mappingPublicRoutes = [
'/',
'/auth/login',
'/auth/forgot',
'/auth/new-password',
];
const mappingRoutePermissions = [
{
path: '/dashboard',
permissions: [],
},
{
path: '/users',
permissions: [PERMISSIONS.USERS.READ_LIST],
},
{
path: '/users/create',
permissions: [PERMISSIONS.USERS.CREATE],
},
{
path: '/users/update',
permissions: [PERMISSIONS.USERS.UPDATE],
},
{
path: '/users/detail',
permissions: [PERMISSIONS.USERS.READ_DETAIL],
},
{
path: '/roles',
permissions: [PERMISSIONS.ROLES.READ_LIST],
},
{
path: '/roles/create',
permissions: [PERMISSIONS.ROLES.CREATE],
},
{
path: '/roles/update',
permissions: [PERMISSIONS.ROLES.UPDATE],
},
{
path: '/roles/detail',
permissions: [PERMISSIONS.ROLES.READ_DETAIL],
},
{
path: '/permissions',
permissions: [PERMISSIONS.PERMISSIONS.READ_LIST],
},
{
path: '/permissions/create',
permissions: [PERMISSIONS.PERMISSIONS.CREATE],
},
{
path: '/permissions/update',
permissions: [PERMISSIONS.PERMISSIONS.UPDATE],
},
{
path: '/permissions/detail',
permissions: [PERMISSIONS.PERMISSIONS.READ_DETAIL],
},
];
//TODO : Fix this later
// const redirectToFirstAccessibleRoute = (userPermissions: string[]) => {
// const fallback = mappingRoutePermissions.find((route) =>
// route.permissions.some((perm) => userPermissions.includes(perm))
// );
// return redirect(fallback?.path ?? '/auth/login');
// };
export const middleware = async ({ request }: LoaderFunctionArgs) => { export const middleware = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url); const url = new URL(request.url);
const pathname = url.pathname; const pathname = url.pathname;
const session = SessionUser.get();
const session_token = SessionToken.get();
const token = session_token?.token?.access_token;
const userPermissions =
session?.role?.permissions?.map?.((perm) => perm?.name) ?? [];
if (pathname) return null; if (mappingPublicRoutes.includes(pathname)) {
if (token) return redirect('/dashboard');
return null;
}
if (!session) return redirect('/auth/login');
const matchedRoute = mappingRoutePermissions.find(
(route) => route.path === pathname
);
if (matchedRoute) {
const hasPermission =
!matchedRoute.permissions ||
matchedRoute.permissions.some((perm) => userPermissions.includes(perm));
if (!hasPermission) {
return '/dashboard';
}
}
return null; return null;
}; };
-6
View File
@@ -6,7 +6,6 @@ import {
TRegisterRequest, TRegisterRequest,
TVerifyEmailRequest, TVerifyEmailRequest,
} from '../../types/auth'; } from '../../types/auth';
import { SessionToken, SessionUser } from '@imphnen-frontend-service/utils';
import { TResponseError, TResponseMessage } from '../../types/common'; import { TResponseError, TResponseMessage } from '../../types/common';
@@ -19,11 +18,6 @@ export const usePostLogin = (): UseMutationResult<
return useMutation({ return useMutation({
mutationKey: ['post-login'], mutationKey: ['post-login'],
mutationFn: async (payload) => await postLogin(payload), mutationFn: async (payload) => await postLogin(payload),
onSuccess: (res) => {
SessionUser.set(res.data.user);
SessionToken.set(res.data.token);
window.location.reload();
},
}); });
}; };
+7 -26
View File
@@ -16,7 +16,7 @@ export const authLoginSchema = z.object({
.min(1, 'Password tidak boleh kosong'), .min(1, 'Password tidak boleh kosong'),
}); });
export const stepOneRegisterSchema = z export const authRegisterSchema = z
.object({ .object({
email: z email: z
.string({ .string({
@@ -25,6 +25,12 @@ export const stepOneRegisterSchema = z
}) })
.min(1, 'Email tidak boleh kosong') .min(1, 'Email tidak boleh kosong')
.email('Email harus valid'), .email('Email harus valid'),
phone_number: z
.string({
required_error: 'Nomor telepon tidak boleh kosong',
})
.min(1, 'Nomor telepon tidak boleh kosong')
.max(15, 'Nomor telepon tidak boleh lebih dari 15 digit'),
fullname: z fullname: z
.string({ .string({
required_error: 'Nama tidak boleh kosong', required_error: 'Nama tidak boleh kosong',
@@ -51,28 +57,3 @@ export const stepOneRegisterSchema = z
message: 'Password dan Konfirmasi Password harus sama', message: 'Password dan Konfirmasi Password harus sama',
path: ['confirm_password'], path: ['confirm_password'],
}); });
const stepTwoRegisterSchema = z.object({
phone_number: z
.string({
required_error: 'Nomor telepon tidak boleh kosong',
invalid_type_error: 'Nomor telepon harus berupa string',
})
.min(1, 'Nomor telepon tidak boleh kosong')
.max(15, 'Nomor telepon tidak boleh lebih dari 15 karakter'),
referral_code: z
.string()
.min(1, 'Kode referral tidak boleh kosong')
.max(4, 'Kode referral tidak boleh lebih dari 4 karakter')
.optional(),
referred_by: z
.string()
.min(1, 'Kode referral tidak boleh kosong')
.max(50, 'Kode referral tidak boleh lebih dari 50 karakter')
.optional(),
student_type: z.string(),
});
export const authRegisterSchema = stepOneRegisterSchema.and(
stepTwoRegisterSchema
);
+17 -20
View File
@@ -1,29 +1,26 @@
import { z } from 'zod';
import { authLoginSchema, authRegisterSchema } from '../../schemas';
import { TResponseDetail, TResponseMessage } from '../common';
import { TUserItem } from '../users'; import { TUserItem } from '../users';
export type TLoginRequest = { export type TTokenItem = {
email: string; access_token: string;
password: string; refresh_token: string;
}; };
export type TLoginResponse = { export type TLoginItem = {
data: { token?: TTokenItem;
token: { user?: TUserItem;
access_token: string;
refresh_token: string;
};
user: TUserItem;
};
}; };
export type TRegisterRequest = { export type TLoginRequest = z.infer<typeof authLoginSchema>;
email: string; export type TLoginResponse = TResponseDetail<TLoginItem>;
fullname: string;
password: string; export type TRegisterRequest = z.infer<typeof authRegisterSchema>;
phone_number: string; export type TRegisterResponse = TResponseDetail<TResponseMessage>;
referral_code?: string;
referred_by?: string; export type TVerifyOtpRequest = {
student_type: string; otp: number;
confirm_password: string;
}; };
export type TVerifyEmailRequest = { export type TVerifyEmailRequest = {
+13 -1
View File
@@ -1,7 +1,19 @@
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
export type TResponse<T = unknown> = { export type TMetaResponse = {
page: number;
per_page: number;
total: number;
};
export type TResponseDetail<T = unknown> = {
data: T; data: T;
message: string;
};
export type TResponseList<T = unknown> = {
data: T[];
meta: TMetaResponse;
}; };
export type TResponseMessage = { export type TResponseMessage = {
+10 -2
View File
@@ -1,9 +1,17 @@
import { TPermissionItem } from '../permissions'; import { TPermissionItem } from '../permissions';
export type TRoleItem = { export type TRoleDetailItem = {
id: string; id: string;
name: string; name: string;
permissions: TPermissionItem[];
created_at: string;
updated_at: string;
};
export type TRolesListItem = {
id: string;
name: string;
permissions_count: number;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
permissions: TPermissionItem[];
}; };
+2 -8
View File
@@ -1,4 +1,4 @@
import { TRoleItem } from '../roles'; import { TRoleDetailItem } from '../roles';
export type TUserItem = { export type TUserItem = {
id: string; id: string;
@@ -7,13 +7,7 @@ export type TUserItem = {
email: string; email: string;
fullname: string; fullname: string;
gender: string; gender: string;
identity_number: string;
is_active: boolean; is_active: boolean;
is_profile_completed: boolean;
phone_number: string; phone_number: string;
referral_code: string; role: TRoleDetailItem;
referred_by: string;
religion: string;
student_type: string;
role: TRoleItem;
}; };
+1
View File
@@ -0,0 +1 @@
export * from './permissions';
+40
View File
@@ -0,0 +1,40 @@
export const PERMISSIONS = {
USERS: {
READ_LIST: 'Read List Users',
READ_DETAIL: 'Read Detail Users',
CREATE: 'Create Users',
UPDATE: 'Update Users',
DELETE: 'Delete Users',
ACTIVATE: 'Activate Users',
},
ROLES: {
READ_LIST: 'Read List Roles',
READ_DETAIL: 'Read Detail Roles',
CREATE: 'Create Roles',
UPDATE: 'Update Roles',
DELETE: 'Delete Roles',
},
PERMISSIONS: {
READ_LIST: 'Read List Permissions',
READ_DETAIL: 'Read Detail Permissions',
CREATE: 'Create Permissions',
UPDATE: 'Update Permissions',
DELETE: 'Delete Permissions',
},
GACHA_CLAIMS: {
CREATE: 'Create Gacha Claims',
READ_DETAIL: 'Read Detail Gacha Claims',
},
GACHA_ITEMS: {
READ_LIST: 'Read List Gacha Items',
READ_DETAIL: 'Read Detail Gacha Items',
CREATE: 'Create Gacha Items',
UPDATE: 'Update Gacha Items',
DELETE: 'Delete Gacha Items',
},
GACHA_ROLLS: {
READ_DETAIL: 'Read Detail Gacha Rolls',
CREATE: 'Create Gacha Rolls',
EXECUTE: 'Execute Gacha Rolls',
},
};
+36
View File
@@ -0,0 +1,36 @@
import Cookies from 'js-cookie';
type TTokenItem = {
access_token: string;
refresh_token: string;
};
const TOKEN_KEY = 'token';
type TStoredToken =
| {
token?: TTokenItem;
}
| undefined;
export const SessionToken = {
set: (val: TStoredToken) => {
Cookies.set(TOKEN_KEY, JSON.stringify(val), {
secure: true,
sameSite: 'Strict',
expires: 7,
});
},
get: (): TStoredToken => {
const token = Cookies.get(TOKEN_KEY);
if (!token) return undefined;
try {
return JSON.parse(token);
} catch {
return undefined;
}
},
remove: () => {
Cookies.remove(TOKEN_KEY);
},
};
+2 -1
View File
@@ -1,3 +1,4 @@
export * from './use-query-state'; export * from './use-query-state';
export * from './use-session'; export * from './use-auth-store';
export * from './use-modal-login'; export * from './use-modal-login';
export * from './use-session';
+47
View File
@@ -0,0 +1,47 @@
import { create } from 'zustand';
import { TLoginItem } from '@imphnen-frontend-service/service';
import { SessionToken } from '../cookies';
import { SessionUser } from '../local-storage';
export enum ESessionStatus {
Authenticated = 'authenticated',
Authenticating = 'authenticating',
Unauthenticated = 'unauthenticated',
}
type SessionState = {
isLoading: boolean;
session?: TLoginItem;
status?: ESessionStatus;
setLoading: (val: boolean) => void;
setSession: (payload: TLoginItem) => void;
clearSession: () => void;
};
export const useAuthStore = create<SessionState>((set) => {
const session = SessionToken.get();
const user = SessionUser.get();
const isAuthenticated = !!session;
return {
isLoading: false,
session: isAuthenticated ? { ...session, ...user } : undefined,
status: isAuthenticated
? ESessionStatus.Authenticated
: ESessionStatus.Unauthenticated,
setLoading: (val) => set({ isLoading: val }),
setSession: (data) => {
SessionToken.set({ token: data.token });
SessionUser.set(data.user);
set({
session: data,
status: ESessionStatus.Authenticated,
});
},
clearSession: () => {
SessionToken.remove();
SessionUser.remove();
set({ session: undefined, status: ESessionStatus.Unauthenticated });
},
};
});
+27 -12
View File
@@ -1,22 +1,37 @@
import { SessionToken, SessionUser } from '../local-storage'; import { TLoginRequest, usePostLogin } from '@imphnen-frontend-service/service';
import { useAuthStore } from './';
import { toast } from 'sonner';
import { useNavigate } from 'react-router';
export const useSession = () => { export const useSession = () => {
const session = { const navigate = useNavigate();
user: SessionUser.get(), const { mutate, isPending } = usePostLogin();
token: SessionToken.get(), const { setLoading, setSession, clearSession, session } = useAuthStore();
const signIn = (payload: TLoginRequest) => {
setLoading(true);
mutate(payload, {
onSuccess: (data) => {
toast.success('Login sukses');
setSession(data.data);
navigate(0);
},
onError: (err) => {
toast.error(
err?.response?.data?.message ??
'Terjadi Kesalahan yang tidak diketahui'
);
clearSession();
},
});
}; };
const isAuthenticated = !!session.token?.access_token;
const signOut = () => { const signOut = () => {
SessionUser.remove(); clearSession();
SessionToken.remove(); navigate(0);
window.location.reload();
}; };
return { return {
isAuthenticated,
session, session,
signIn,
signOut, signOut,
isLoading: isPending,
}; };
}; };
+3
View File
@@ -3,3 +3,6 @@ export * from './react-router';
export * from './tailwind-merge'; export * from './tailwind-merge';
export * from './hooks'; export * from './hooks';
export * from './local-storage'; export * from './local-storage';
export * from './cookies';
export * from './session';
export * from './constants';
+1 -26
View File
@@ -20,41 +20,16 @@ type TUserItem = {
email: string; email: string;
fullname: string; fullname: string;
gender: string; gender: string;
identity_number: string;
is_active: boolean; is_active: boolean;
is_profile_completed: boolean;
phone_number: string; phone_number: string;
referral_code: string;
referred_by: string;
religion: string;
student_type: string;
role: TRoleItem; role: TRoleItem;
}; };
export const SessionUser = { export const SessionUser = {
set: (val: TUserItem) => localStorage.setItem('users', JSON.stringify(val)), set: (val?: TUserItem) => localStorage.setItem('users', JSON.stringify(val)),
get: (): TUserItem | undefined => { get: (): TUserItem | undefined => {
const users = localStorage.getItem('users'); const users = localStorage.getItem('users');
return users ? JSON.parse(users) : undefined; return users ? JSON.parse(users) : undefined;
}, },
remove: () => localStorage.removeItem('users'), remove: () => localStorage.removeItem('users'),
}; };
export const SessionToken = {
set: (val: { access_token: string; refresh_token: string }) => {
localStorage.setItem('access_token', val.access_token);
localStorage.setItem('refresh_token', val.refresh_token);
},
get: ():
| { access_token?: string | null; refresh_token?: string | null }
| undefined => {
return {
access_token: localStorage.getItem('access_token'),
refresh_token: localStorage.getItem('refresh_token'),
};
},
remove: () => {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
},
};
@@ -1,15 +1,22 @@
import { lazy, LazyExoticComponent, ReactElement } from 'react'; import { lazy, LazyExoticComponent, ReactNode } from 'react';
import { ActionFunction, LoaderFunction, RouteObject } from 'react-router'; import { ActionFunction, LoaderFunction, RouteObject } from 'react-router';
type TPermissionItem = {
id: string;
name: string;
created_at: string;
updated_at: string;
};
interface PageModuleExports { interface PageModuleExports {
default: () => ReactElement; default: () => ReactNode;
loader?: LoaderFunction; loader?: LoaderFunction;
action?: ActionFunction; action?: ActionFunction;
permissions?: Array<string>; permissions?: Array<string>;
} }
interface LoadingModuleExports { interface LoadingModuleExports {
default: () => ReactElement; default: () => ReactNode;
} }
interface RouteHandle { interface RouteHandle {
@@ -38,7 +45,6 @@ export function convertPagesToRoute(
filePath, filePath,
loadingFiles loadingFiles
); );
const route = createRoute({ const route = createRoute({
PageComponent: page, PageComponent: page,
LoadingComponent: loadingComponent, LoadingComponent: loadingComponent,
@@ -51,6 +57,20 @@ export function convertPagesToRoute(
const result = (await importer()) as PageModuleExports; const result = (await importer()) as PageModuleExports;
return 'loader' in result ? result.loader?.(args) : null; return 'loader' in result ? result.loader?.(args) : null;
}, },
async guard() {
const result = (await importer()) as PageModuleExports;
const localStoragePermission = localStorage.getItem('permissions');
const permissions: TPermissionItem[] | undefined =
localStoragePermission
? JSON.parse(localStoragePermission)
: undefined;
return 'permissions' in result
? result.permissions?.every(
(permission) =>
permissions?.some((item) => permission === item.name) || false
) || false
: true;
},
}); });
routes = mergeRoutes(routes, route); routes = mergeRoutes(routes, route);
}); });
@@ -62,19 +82,14 @@ function findMatchingLoadingComponent(
loadingFiles: Record<string, () => Promise<unknown>> loadingFiles: Record<string, () => Promise<unknown>>
) { ) {
const loadingPath = filePath.replace(/(page|layout)\.tsx$/, 'loading.tsx'); const loadingPath = filePath.replace(/(page|layout)\.tsx$/, 'loading.tsx');
const groupMatch = filePath.match(/\([^/]+\//); const groupMatch = filePath.match(/\([^/]+\//);
const groupLoadingPath = groupMatch ? `/${groupMatch[0]}loading.tsx` : null; const groupLoadingPath = groupMatch ? `/${groupMatch[0]}loading.tsx` : null;
const globalLoadingPath = './app/loading.tsx'; const globalLoadingPath = './app/loading.tsx';
const loader = const loader =
loadingFiles[loadingPath] || loadingFiles[loadingPath] ||
(groupLoadingPath && loadingFiles[groupLoadingPath]) || (groupLoadingPath && loadingFiles[groupLoadingPath]) ||
loadingFiles[globalLoadingPath]; loadingFiles[globalLoadingPath];
if (!loader) return undefined; if (!loader) return undefined;
return lazy(loader as () => Promise<LoadingModuleExports>); return lazy(loader as () => Promise<LoadingModuleExports>);
} }
@@ -86,68 +101,110 @@ function mergeRoutes(
throw new Error( throw new Error(
`Paths do not match: "${target.path}" and "${source.path}"` `Paths do not match: "${target.path}" and "${source.path}"`
); );
target.children = target.children || [];
if (source.handle?.pageType === 'layout') {
return handleLayoutMerge(target, source);
}
if (source.handle?.pageType === 'page') {
return handlePageMerge(target, source);
}
if (source.children && source.children.length > 0) {
if (target.handle?.pageType === 'page') {
if (!target.children?.some((child) => child.index)) {
target.children = target.children || [];
target.children.unshift({
index: true,
element: target.element,
HydrateFallback: target.HydrateFallback,
action: target.action,
loader: target.loader,
handle: target.handle,
errorElement: target.errorElement,
});
}
delete target.element;
delete target.action;
delete target.loader;
delete target.handle;
}
mergeChildRoutes(target, source);
}
return target;
}
export function mergeChildRoutes(
target: ExtendedRouteObject,
source: ExtendedRouteObject
): void {
if (!source.children) return;
if (!target.children) { if (!target.children) {
target.children = []; target.children = [];
} }
source.children.forEach((sourceChild) => {
const matchingChild = target.children!.find(
(targetChild) => targetChild.path === sourceChild.path
);
if (source.handle?.pageType === 'layout') { if (matchingChild) {
if (!target.element) { mergeRoutes(matchingChild, sourceChild);
target.element = source.element; } else {
target.HydrateFallback = source.HydrateFallback; target.children!.push(sourceChild);
target.action = source.action;
target.loader = source.loader;
target.handle = source.handle;
target.errorElement = source.errorElement;
target.children = target.children ?? [];
} else if (target.handle?.pageType === 'page') {
target = swapTargetRouteAsIndexRouteAndUpdateWithRoute(target, source);
} }
return target; });
} }
if ( export function handleLayoutMerge(
source.handle?.pageType === 'page' && target: ExtendedRouteObject,
!target.children.some((child) => child.index) source: ExtendedRouteObject
) { ): ExtendedRouteObject {
target.children.unshift({ if (!target.element) {
index: true, Object.assign(target, {
element: source.element, element: source.element,
HydrateFallback: source.HydrateFallback, HydrateFallback: source.HydrateFallback,
action: source.action, action: source.action,
loader: source.loader, loader: source.loader,
handle: source.handle, handle: source.handle,
errorElement: source.errorElement,
}); });
return target; } else if (target.handle?.pageType === 'page') {
target = swapTargetRouteAsIndexRouteAndUpdateWithRoute(target, source);
} }
if (
target.handle?.pageType === 'layout' &&
source.handle?.pageType === 'page'
) {
target = addRouteAsIndexRouteForTargetRoute(target, source);
return target;
}
if (source.children) {
target.children = target.children ?? [];
source.children.forEach((sourceChild) => {
const matchingChild = target.children?.find(
(targetChild) => targetChild.path === sourceChild.path
);
if (matchingChild) mergeRoutes(matchingChild, sourceChild);
else target.children?.push(sourceChild);
});
}
return target; return target;
} }
function swapTargetRouteAsIndexRouteAndUpdateWithRoute( export function handlePageMerge(
target: ExtendedRouteObject, target: ExtendedRouteObject,
route: ExtendedRouteObject source: ExtendedRouteObject
): ExtendedRouteObject { ): ExtendedRouteObject {
target.children = target.children ?? []; if (!target.children) {
target.children = [];
}
if (
!target.children.some((child) => child.index) ||
target.handle?.pageType === 'layout'
) {
if (target.handle?.pageType === 'layout') {
addRouteAsIndexRouteForTargetRoute(target, source);
} else {
target.children.unshift({
index: true,
element: source.element,
HydrateFallback: source.HydrateFallback,
action: source.action,
loader: source.loader,
handle: source.handle,
errorElement: source.errorElement,
});
}
}
return target;
}
export function swapTargetRouteAsIndexRouteAndUpdateWithRoute(
target: ExtendedRouteObject,
layout: ExtendedRouteObject
): ExtendedRouteObject {
target.children = target.children || [];
target.children.push({ target.children.push({
index: true, index: true,
element: target.element, element: target.element,
@@ -157,38 +214,38 @@ function swapTargetRouteAsIndexRouteAndUpdateWithRoute(
handle: target.handle, handle: target.handle,
errorElement: target.errorElement, errorElement: target.errorElement,
}); });
Object.assign(target, {
target.element = route.element; element: layout.element,
target.HydrateFallback = route.HydrateFallback; HydrateFallback: layout.HydrateFallback,
target.action = route.action; action: layout.action,
target.loader = route.loader; loader: layout.loader,
target.handle = route.handle; handle: layout.handle,
target.errorElement = route.errorElement; errorElement: layout.errorElement,
});
return target; return target;
} }
function addRouteAsIndexRouteForTargetRoute( export function addRouteAsIndexRouteForTargetRoute(
target: ExtendedRouteObject, target: ExtendedRouteObject,
route: ExtendedRouteObject page: ExtendedRouteObject
): ExtendedRouteObject { ): ExtendedRouteObject {
target.children = target.children ?? []; target.children = target.children || [];
target.children.push({ target.children.push({
index: true, index: true,
element: route.element, element: page.element,
HydrateFallback: route.HydrateFallback, HydrateFallback: page.HydrateFallback,
action: route.action, action: page.action,
loader: route.loader, loader: page.loader,
handle: route.handle, handle: page.handle,
errorElement: route.errorElement, errorElement: page.errorElement,
}); });
return target; return target;
} }
function createRoute(args: { function createRoute(args: {
segments: string[]; segments: string[];
PageComponent: LazyExoticComponent<() => ReactElement>; PageComponent: LazyExoticComponent<() => ReactNode>;
LoadingComponent?: LazyExoticComponent<() => ReactElement>; LoadingComponent?: LazyExoticComponent<() => ReactNode>;
loader?: LoaderFunction; loader?: LoaderFunction;
action?: ActionFunction; action?: ActionFunction;
guard?: () => Promise<boolean>; guard?: () => Promise<boolean>;
@@ -196,21 +253,24 @@ function createRoute(args: {
const [current, ...rest] = args.segments; const [current, ...rest] = args.segments;
const [cleanPath, pageType] = current.split(separator); const [cleanPath, pageType] = current.split(separator);
const route: ExtendedRouteObject = { path: cleanPath }; const route: ExtendedRouteObject = { path: cleanPath };
if (pageType === 'page' || pageType === 'layout') { if (pageType === 'page' || pageType === 'layout') {
route.element = <args.PageComponent />; route.element = <args.PageComponent />;
route.HydrateFallback = route.HydrateFallback =
args.LoadingComponent ?? (() => <div>Loading...</div>); args.LoadingComponent ?? (() => <div>Loading...</div>);
route.action = args.action; route.action = args.action;
route.loader = async (...props) => { route.loader = async (...props) => {
if (!(await args.guard?.())) {
throw new Response('Forbidden', {
status: 403,
statusText: 'Forbidden',
});
}
return args.loader?.(...props); return args.loader?.(...props);
}; };
route.handle = { pageType: pageType as 'layout' | 'page' }; route.handle = { pageType: pageType as 'layout' | 'page' };
} }
if (rest.length > 0) { if (rest.length > 0) {
const nextSegment = rest[0].split(separator)[0]; const nextSegment = rest[0].split(separator)[0];
if (nextSegment === 'update' || nextSegment === 'edit') { if (nextSegment === 'update' || nextSegment === 'edit') {
return { return {
path: `${cleanPath}/${nextSegment}`, path: `${cleanPath}/${nextSegment}`,
@@ -221,20 +281,16 @@ function createRoute(args: {
handle: { pageType: pageType as 'layout' | 'page' }, handle: { pageType: pageType as 'layout' | 'page' },
}; };
} }
const childRoute = createRoute({ ...args, segments: rest }); const childRoute = createRoute({ ...args, segments: rest });
if (!route.children) { if (!route.children) {
route.children = []; route.children = [];
} }
if (cleanPath.startsWith(':')) { if (cleanPath.startsWith(':')) {
route.children.unshift(childRoute); route.children.unshift(childRoute);
} else { } else {
route.children.push(childRoute); route.children.push(childRoute);
} }
} }
return route; return route;
} }
@@ -258,7 +314,6 @@ export function getRouteSegmentsFromFilePath(
if (segment.startsWith('[')) return getParamFromSegment(segment); if (segment.startsWith('[')) return getParamFromSegment(segment);
return segment; return segment;
}); });
return getRouteSegments(segments[0], segments, transformer); return getRouteSegments(segments[0], segments, transformer);
} }
@@ -306,7 +361,7 @@ export function addErrorElementToRoutes(
(_, prevSegment) => prevSegment (_, prevSegment) => prevSegment
); );
const ErrorBoundary = lazy( const ErrorBoundary = lazy(
importer as () => Promise<{ default: () => ReactElement }> importer as () => Promise<{ default: () => ReactNode }>
); );
setRoute(segments, routes, (route) => { setRoute(segments, routes, (route) => {
route.errorElement = <ErrorBoundary />; route.errorElement = <ErrorBoundary />;
@@ -325,7 +380,7 @@ export function add404PageToRoutesChildren(
(_, prevSegment) => prevSegment (_, prevSegment) => prevSegment
); );
const NotFound = lazy( const NotFound = lazy(
importer as () => Promise<{ default: () => ReactElement }> importer as () => Promise<{ default: () => ReactNode }>
); );
setRoute(segments, routes, (route) => { setRoute(segments, routes, (route) => {
if (route.children) { if (route.children) {
@@ -340,9 +395,7 @@ export function add404PageToRoutesChildren(
action: tempRoute.action, action: tempRoute.action,
loader: tempRoute.loader, loader: tempRoute.loader,
}); });
route.children.push({ path: '*', element: <NotFound /> }); route.children.push({ path: '*', element: <NotFound /> });
delete route.element; delete route.element;
delete route.action; delete route.action;
delete route.loader; delete route.loader;
@@ -352,7 +405,7 @@ export function add404PageToRoutesChildren(
}); });
} }
function set404NonPage(routes: RouteObject, notFoundElement: ReactElement) { function set404NonPage(routes: RouteObject, notFoundElement: ReactNode) {
if ( if (
routes.path && routes.path &&
routes.children?.length && routes.children?.length &&
@@ -377,7 +430,6 @@ function setRoute(
segments.forEach((_segment, i) => { segments.forEach((_segment, i) => {
const isLastSegment = i === segments.length - 1; const isLastSegment = i === segments.length - 1;
if (isLastSegment) return (temp = updater(temp)); if (isLastSegment) return (temp = updater(temp));
if (!isLastSegment) { if (!isLastSegment) {
const nextSegment = segments[i + 1]; const nextSegment = segments[i + 1];
const index = temp.children?.findIndex( const index = temp.children?.findIndex(
+27
View File
@@ -0,0 +1,27 @@
import { FC, PropsWithChildren, ReactNode, useMemo } from 'react';
import { useSession } from '../../hooks/use-session';
type TProps = PropsWithChildren<{
permissions: Array<string>;
fallback?: ReactNode;
}>;
export const Guard: FC<TProps> = (props): ReactNode => {
const { session } = useSession();
const permissionKeys = useMemo(() => {
return session?.user?.role?.permissions.map((perm) => perm.name) ?? [];
}, [session?.user?.role]);
const allowed = useMemo(() => {
return props.permissions.every((permission) =>
permissionKeys.includes(permission)
);
}, [permissionKeys, props.permissions]);
if (allowed) {
return props.children;
}
return props.fallback ?? null;
};
+1
View File
@@ -0,0 +1 @@
export * from './guard';
+31 -1
View File
@@ -38,7 +38,8 @@
"sharp": "^0.34.1", "sharp": "^0.34.1",
"sonner": "^2.0.3", "sonner": "^2.0.3",
"tailwind-merge": "^3.0.2", "tailwind-merge": "^3.0.2",
"zod": "^3.24.2" "zod": "^3.24.2",
"zustand": "^5.0.5"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.14.5", "@babel/core": "^7.14.5",
@@ -40387,6 +40388,35 @@
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
}, },
"node_modules/zustand": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.5.tgz",
"integrity": "sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=18.0.0",
"immer": ">=9.0.6",
"react": ">=18.0.0",
"use-sync-external-store": ">=1.2.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
},
"use-sync-external-store": {
"optional": true
}
}
},
"node_modules/zwitch": { "node_modules/zwitch": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+5 -4
View File
@@ -3,15 +3,15 @@
"version": "0.0.0", "version": "0.0.0",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"gacha:dev": "nx serve gacha", "gacha:dev": "nx dev gacha",
"gacha:build": "nx build gacha", "gacha:build": "nx build gacha",
"gacha:test": "nx test gacha", "gacha:test": "nx test gacha",
"gacha:prod": "serve ./dist/apps/gacha", "gacha:prod": "serve ./dist/apps/gacha",
"backoffice:dev": "nx serve backoffice", "backoffice:dev": "nx dev backoffice",
"backoffice:build": "nx build backoffice", "backoffice:build": "nx build backoffice",
"backoffice:test": "nx test backoffice", "backoffice:test": "nx test backoffice",
"backoffice:prod": "serve ./dist/apps/backoffice", "backoffice:prod": "serve ./dist/apps/backoffice",
"dimentorin:dev": "nx serve dimentorin", "dimentorin:dev": "nx dev dimentorin",
"dimentorin:test": "nx test dimentorin", "dimentorin:test": "nx test dimentorin",
"dimentorin:build": "nx build dimentorin", "dimentorin:build": "nx build dimentorin",
"dimentorin:prod": "serve ./dist/apps/dimentorin", "dimentorin:prod": "serve ./dist/apps/dimentorin",
@@ -55,7 +55,8 @@
"sharp": "^0.34.1", "sharp": "^0.34.1",
"sonner": "^2.0.3", "sonner": "^2.0.3",
"tailwind-merge": "^3.0.2", "tailwind-merge": "^3.0.2",
"zod": "^3.24.2" "zod": "^3.24.2",
"zustand": "^5.0.5"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.14.5", "@babel/core": "^7.14.5",