Merge branch 'develop' of https://github.com/IMPHNEN/imphnen-frontend-service into develop
merge develop
This commit is contained in:
commit
af8968ac01
157 files changed
+21740
-15479
No files matched your search
@@ -1 +0,0 @@
|
|||||||
VITE_API_URL=https://api.example.com
|
|
||||||
@@ -24,5 +24,14 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
|
- name: Set env vars
|
||||||
|
run: |
|
||||||
|
echo "CMS_SECRET=${{ secrets.CMS_SECRET }}" >> $GITHUB_ENV
|
||||||
|
echo "CMS_STORAGE_REGION=${{ secrets.CMS_STORAGE_REGION }}" >> $GITHUB_ENV
|
||||||
|
echo "CMS_STORAGE_ENDPOINT=${{ secrets.CMS_STORAGE_ENDPOINT }}" >> $GITHUB_ENV
|
||||||
|
echo "CMS_STORAGE_ACCESS_KEY_ID=${{ secrets.CMS_STORAGE_ACCESS_KEY_ID }}" >> $GITHUB_ENV
|
||||||
|
echo "CMS_STORAGE_SECRET_ACCESS_KEY=${{ secrets.CMS_STORAGE_SECRET_ACCESS_KEY }}" >> $GITHUB_ENV
|
||||||
|
echo "CMS_POSTGRES_URL=${{ secrets.CMS_POSTGRES_URL }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Nx Build
|
- name: Nx Build
|
||||||
run: npx nx run-many --target=build --all
|
run: npx nx run-many --target=build --all
|
||||||
+5
-1
@@ -57,4 +57,8 @@ storybook-static
|
|||||||
.data
|
.data
|
||||||
.nuxt
|
.nuxt
|
||||||
.nitro
|
.nitro
|
||||||
.cache
|
.cache
|
||||||
|
|
||||||
|
# Next.js
|
||||||
|
.next
|
||||||
|
out
|
||||||
Vendored
+2
-1
@@ -2,6 +2,7 @@
|
|||||||
"recommendations": [
|
"recommendations": [
|
||||||
"nrwl.angular-console",
|
"nrwl.angular-console",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"dbaeumer.vscode-eslint"
|
"dbaeumer.vscode-eslint",
|
||||||
|
"ms-playwright.playwright"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -6,9 +6,10 @@
|
|||||||
|
|
||||||
This repository is a **monorepo** for all frontend services of IMPHNEN. The monorepo includes three main applications:
|
This repository is a **monorepo** for all frontend services of IMPHNEN. The monorepo includes three main applications:
|
||||||
|
|
||||||
1. **Gacha** - Application for <a href="https://gacha.imphnen.dev/" target="_blank">gacha website</a>.
|
1. **Gacha** - Application for <a href="https://gacha.imphnen.dev/" target="_blank">Gacha Website</a>.
|
||||||
2. **Backoffice** - Application for internal management.
|
2. **Backoffice** - Application for <a href="https://gacha.imphnen.dev/" target="_blank">Internal Management Website</a>.
|
||||||
3. **Dimentorin** - Application for mentoring services.
|
3. **Dimentorin** - Application for <a href="https://dimentorin.imphnen.dev/" target="_blank">Mentoring Service</a>.
|
||||||
|
4. **Landing Page** - Application for <a href="https://imphnen.dev/" target="_blank">Landing Page</a>.
|
||||||
|
|
||||||
## How to install
|
## How to install
|
||||||
|
|
||||||
@@ -24,6 +25,15 @@ This repository is a **monorepo** for all frontend services of IMPHNEN. The mono
|
|||||||
|
|
||||||
## How to run
|
## How to run
|
||||||
|
|
||||||
|
### Setup Environment Variables
|
||||||
|
|
||||||
|
Before running the applications, you need to set up the environment variables. You can do this by copying the `.env.example` file to `.env` and modifying the values according to your needs.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd apps/{appname}
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
Use the following commands to run in development mode:
|
Use the following commands to run in development mode:
|
||||||
@@ -40,6 +50,10 @@ Use the following commands to run in development mode:
|
|||||||
```sh
|
```sh
|
||||||
npm run dimentorin:dev
|
npm run dimentorin:dev
|
||||||
```
|
```
|
||||||
|
- **Landing Page**:
|
||||||
|
```sh
|
||||||
|
npm run landing:dev
|
||||||
|
```
|
||||||
|
|
||||||
### Build
|
### Build
|
||||||
|
|
||||||
@@ -57,6 +71,10 @@ Use the following commands to build the applications:
|
|||||||
```sh
|
```sh
|
||||||
npm run dimentorin:build
|
npm run dimentorin:build
|
||||||
```
|
```
|
||||||
|
- **Landing Page**:
|
||||||
|
```sh
|
||||||
|
npm run landing:build
|
||||||
|
```
|
||||||
|
|
||||||
### Production
|
### Production
|
||||||
|
|
||||||
@@ -74,19 +92,22 @@ Use the following commands to run the applications in production mode:
|
|||||||
```sh
|
```sh
|
||||||
npm run dimentorin:prod
|
npm run dimentorin:prod
|
||||||
```
|
```
|
||||||
|
- **Landing Page**:
|
||||||
|
```sh
|
||||||
|
npm run landing:prod
|
||||||
|
```
|
||||||
|
|
||||||
### Storybook
|
### Storybook
|
||||||
|
|
||||||
This repository uses Storybook to develop, test, and document UI components in an isolated and interactive environment. Below are the commands to work with Storybook:
|
This repository uses Storybook to develop, test, and document UI components in an isolated and interactive environment. Below are the commands to work with Storybook:
|
||||||
|
|
||||||
- **Run Storybook**
|
- **Run Storybook**
|
||||||
|
|
||||||
This command starts Storybook in development mode, allowing you to view and test UI components interactively.
|
This command starts Storybook in development mode, allowing you to view and test UI components interactively.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run ui:storybook
|
npm run ui:storybook
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
- **Run Unit Test**
|
- **Run Unit Test**
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_URL=
|
||||||
File renamed without changes.
+2
-2
@@ -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>
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -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);
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+4
-9
@@ -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,
|
||||||
+10
-1
@@ -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>
|
||||||
File renamed without changes.
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
||||||
};
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_URL=
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import {
|
||||||
|
authLoginSchema,
|
||||||
|
TLoginRequest,
|
||||||
|
} from '@imphnen-frontend-service/service';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useSession } from '@imphnen-frontend-service/utils';
|
||||||
|
|
||||||
|
export const useLogin = () => {
|
||||||
|
const form = useForm<TLoginRequest>({
|
||||||
|
resolver: zodResolver(authLoginSchema),
|
||||||
|
mode: 'all',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { signIn } = useSession();
|
||||||
|
|
||||||
|
const onSubmit = form.handleSubmit((data) => signIn(data));
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
onSubmit,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,46 +1,50 @@
|
|||||||
import { FC, ReactElement } from 'react';
|
import { FC, ReactElement } from 'react';
|
||||||
import { LoginBanner } from '@imphnen-frontend-service/ui/organisms';
|
import { ControlledInputField, LoginBanner } from '@imphnen-frontend-service/ui/organisms';
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useLogin } from '../../_hooks/use-login';
|
||||||
import { InputField } from '@imphnen-frontend-service/ui/molecules';
|
import { ArrowLeftOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
export const Components: FC = (): ReactElement => {
|
||||||
const [searchParams] = useSearchParams();
|
const { form, onSubmit } = useLogin();
|
||||||
const error = searchParams.get('error'); // Ambil nilai ?error=
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
|
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
|
||||||
<div className="bg-white min-w-[1120px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6">
|
<div className="bg-white xl:min-w-[1120px] xl:min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6">
|
||||||
<LoginBanner />
|
<LoginBanner />
|
||||||
<div className="border-2 border-primary-500/50 w-[596px] rounded-lg py-[70px] px-[96px] flex justify-center">
|
<div className="xl:border-2 xl:border-primary-500/50 md:w-[596px] rounded-lg md:py-[70px] md:px-[96px] flex justify-center">
|
||||||
<div className="w-[404px]">
|
<div className="md:w-[404px]">
|
||||||
<h2 className="text-4xl font-semibold text-primary-500 text-center mb-2">
|
<h2 className="text-4xl font-semibold text-primary-500 text-center mb-2">
|
||||||
Hallo Minna-san
|
Hallo Minna-san
|
||||||
</h2>
|
</h2>
|
||||||
<h5 className="text-xl font-medium text-primary-500 text-center">
|
<h5 className="text-xl font-medium text-primary-500 text-center mb-5">
|
||||||
Welcome to Dimentorin by IMPHNEN
|
Welcome to Dimentorin by IMPHNEN
|
||||||
</h5>
|
</h5>
|
||||||
<InputField
|
<form onSubmit={onSubmit} className="flex flex-col gap-2">
|
||||||
label="Email"
|
<ControlledInputField
|
||||||
error={error ? error : undefined}
|
control={form.control}
|
||||||
size="lg"
|
label="Email"
|
||||||
className="w-full"
|
size="lg"
|
||||||
placeholder="Masukkan email-mu, Senpai~! ✨ (Pastikan tidak typo, ya~ 😆)"
|
className="w-full mb-2"
|
||||||
/>
|
placeholder="Masukkan email-mu, Senpai~! ✨ (Pastikan tidak typo, ya~ 😆)"
|
||||||
<InputField
|
name={'email'}
|
||||||
label="password"
|
/>
|
||||||
error={error ? error : undefined}
|
<ControlledInputField
|
||||||
className="w-full"
|
control={form.control}
|
||||||
size="lg"
|
label="Password"
|
||||||
type="password"
|
className="w-full"
|
||||||
placeholder="Masukkan password rahasiamu!"
|
size="lg"
|
||||||
/>
|
type="password"
|
||||||
<div className="flex justify-end my-5">
|
placeholder="Masukkan password rahasiamu!"
|
||||||
<a href="/auth/forgot" className="text-primary-500 font-medium">
|
name={'password'}
|
||||||
Lupa Password ?
|
/>
|
||||||
</a>
|
<div className="flex justify-end my-5">
|
||||||
</div>
|
<a href="/auth/forgot" className="text-primary-500 font-medium">
|
||||||
<Button className="w-full">Enter Isekai</Button>
|
Lupa Password ?
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<Button className="w-full" type='submit'>Enter Isekai</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
<div className="flex my-3 gap-3 justify-center">
|
<div className="flex my-3 gap-3 justify-center">
|
||||||
<h5>Belum Punya akun ?</h5>
|
<h5>Belum Punya akun ?</h5>
|
||||||
<a href="/auth/register" className="text-primary-500 font-medium">
|
<a href="/auth/register" className="text-primary-500 font-medium">
|
||||||
@@ -63,6 +67,13 @@ export const Components: FC = (): ReactElement => {
|
|||||||
width={24}
|
width={24}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
className='xl:hidden w-full gap-3'
|
||||||
|
variant='secondary'
|
||||||
|
>
|
||||||
|
<ArrowLeftOutlined />
|
||||||
|
Kembali Ke Homepage
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export const Components: FC = (): ReactElement => {
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h4 className="font-medium">OTP Code</h4>
|
<h4 className="font-medium">OTP Code</h4>
|
||||||
<Input
|
<Input
|
||||||
widthform="custom"
|
|
||||||
className="w-full"
|
className="w-full"
|
||||||
size="lg"
|
size="lg"
|
||||||
placeholder="Kode OTP"
|
placeholder="Kode OTP"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
convertPagesToRoute,
|
convertPagesToRoute,
|
||||||
QueryProvider,
|
QueryProvider,
|
||||||
} from '@imphnen-frontend-service/utils';
|
} from '@imphnen-frontend-service/utils';
|
||||||
|
import { Toaster } from 'sonner';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
const files = import.meta.glob('./app/**/*(page|layout).tsx');
|
const files = import.meta.glob('./app/**/*(page|layout).tsx');
|
||||||
@@ -34,6 +35,7 @@ if (!rootElement) throw new Error('Failed to find the root element');
|
|||||||
createRoot(rootElement).render(
|
createRoot(rootElement).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<QueryProvider>
|
<QueryProvider>
|
||||||
|
<Toaster position="top-right" />
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_URL=
|
||||||
Loaded 50 of 157 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user