Compare commits

..
64 Commits
Author SHA1 Message Date
ArraysID a84e2fc31a fix: remove mistakenly added env from test-build workflow 2025-05-11 05:53:31 +07:00
ArraysID 6e1c561387 Merge branch 'feat/landing-cms' of https://github.com/IMPHNEN/imphnen-frontend-service into feat/landing-cms 2025-05-11 05:36:24 +07:00
ArraysID b39e918b3c fix: remove unused environment variables from test build workflow 2025-05-11 05:32:55 +07:00
ArraysID 0a217341a5 chore: add environment placeholder variables for CMS configuration in test build workflow 2025-05-11 05:32:55 +07:00
ArraysID f1f72402b2 fix: add placeholder to .env.example 2025-05-11 05:32:55 +07:00
ArraysID 3f1b2a2bd7 fix: supress scss deps warning 2025-05-11 05:32:55 +07:00
ArraysID 57b27c6784 feat: wrap logo image in a link to navigate to the homepage 2025-05-11 05:32:55 +07:00
ArraysID f191ffc4b1 feat: update button components to use onClick for external links and improve styling 2025-05-11 05:32:25 +07:00
ArraysID 31e4ffae92 feat: update CallToActionSection label and wrap Discord button in link 2025-05-11 05:29:34 +07:00
ArraysID c9f7ce24ed feat: implement CallToAction section with dynamic data fetching and UI integration 2025-05-11 05:29:01 +07:00
ArraysID d21adebf73 feat: simplify joinTitle rendering in Testimonials section 2025-05-11 05:28:20 +07:00
ArraysID 744d39ea75 feat: implement Testimonials section with dynamic data fetching and UI integration 2025-05-11 05:28:20 +07:00
ArraysID da11ba5894 refactor: reorganize layout structure by moving Header and Footer components to RootLayout 2025-05-11 05:27:19 +07:00
ArraysID 6d29c81dad feat: enhance LearningResourcesSection with primary and secondary button links 2025-05-11 05:26:07 +07:00
ArraysID 4a7135dd17 feat: add Learning Resources section with dynamic data fetching and UI integration 2025-05-11 05:24:31 +07:00
ArraysID 33d750b221 feat: add heroImage field to HeroSection and implement image formatting utility 2025-05-11 05:24:31 +07:00
ArraysID 1f9aa2e6b3 feat: update FeaturesSection to use Iconify icons and adjust icon properties 2025-05-11 05:24:31 +07:00
ArraysID 7722c98c84 feat: replace Link component with Button for improved button styling in Community component 2025-05-11 05:24:31 +07:00
ArraysID 0914361635 feat: update Community component to use Iconify for icons and adjust CommunitiesSection configuration 2025-05-11 05:24:31 +07:00
ArraysID 74aabd1eba feat: add support for s3 bucket for media 2025-05-11 05:24:31 +07:00
ArraysID d1fc7a8fd5 refactor: move global section functions to services directory 2025-05-11 05:24:31 +07:00
ArraysID 009cabfc1c feat: add CommunitiesSection configuration and integrate with Community component 2025-05-11 05:24:31 +07:00
ArraysID a7f7521761 fix: update revalidation interval to 10 seconds 2025-05-11 05:24:31 +07:00
ArraysID 54b7a2f0eb refactor: optimize data fetching by using Promise.all 2025-05-11 05:24:31 +07:00
ArraysID ef042f2f40 feat: add revalidate constant to control page revalidation interval 2025-05-11 05:24:31 +07:00
ArraysID fd35cb9525 feat: add FeaturesSection global configuration and integrate with Features component 2025-05-11 05:24:31 +07:00
ArraysID 18d11fd1c3 feat: implement HeroSection global configuration and integrate with Hero component 2025-05-11 05:24:31 +07:00
ArraysID c82d9bcc70 fix: include withPayload in Next.js plugins for proper configuration 2025-05-11 05:24:31 +07:00
ArraysID a4dd793bb7 refactor: update config import paths to use local payload.config 2025-05-11 05:24:31 +07:00
ArraysID bba3c89b23 fix: update TypeScript paths for payload configuration 2025-05-11 05:24:31 +07:00
ArraysID 8b6aa21f82 feat: add payload configuration and update TypeScript target 2025-05-11 05:24:31 +07:00
ArraysID 0fffe2d403 feat: integrate payload cms to landing page 2025-05-11 05:24:09 +07:00
ArraysID 919fac811d fix: remove unused environment variables from test build workflow 2025-05-10 17:07:51 +07:00
ArraysID 4316dcf8b3 chore: add environment placeholder variables for CMS configuration in test build workflow 2025-05-10 17:04:33 +07:00
ArraysID b9b71c4c25 fix: add placeholder to .env.example 2025-05-10 16:50:26 +07:00
ArraysID 121faf89c3 fix: supress scss deps warning 2025-05-10 16:26:32 +07:00
ArraysID 07913efb16 Merge branch 'develop' into feat/landing-cms 2025-05-10 16:01:05 +07:00
ArraysID 4b2733511c feat: wrap logo image in a link to navigate to the homepage 2025-05-10 15:50:42 +07:00
ArraysID 9f3d00a0db feat: update button components to use onClick for external links and improve styling 2025-05-10 15:48:35 +07:00
ArraysID 7ac571c019 feat: update CallToActionSection label and wrap Discord button in link 2025-05-10 15:34:18 +07:00
ArraysID a8c958400d feat: implement CallToAction section with dynamic data fetching and UI integration 2025-05-10 15:19:54 +07:00
ArraysID 760ab67ae0 feat: simplify joinTitle rendering in Testimonials section 2025-05-10 14:56:07 +07:00
ArraysID c4cf9e4e6c feat: implement Testimonials section with dynamic data fetching and UI integration 2025-05-10 14:52:35 +07:00
ArraysID a800b1e293 refactor: reorganize layout structure by moving Header and Footer components to RootLayout 2025-05-10 13:20:43 +07:00
ArraysID da0e599b9a feat: enhance LearningResourcesSection with primary and secondary button links 2025-05-10 12:58:34 +07:00
ArraysID a02a259f4e feat: add Learning Resources section with dynamic data fetching and UI integration 2025-05-10 12:48:07 +07:00
ArraysID ff99564b05 feat: add heroImage field to HeroSection and implement image formatting utility 2025-05-10 08:54:45 +07:00
ArraysID 13ab089afc feat: update FeaturesSection to use Iconify icons and adjust icon properties 2025-05-10 07:45:29 +07:00
ArraysID 378e258520 feat: replace Link component with Button for improved button styling in Community component 2025-05-10 07:30:06 +07:00
ArraysID 88468081b6 feat: update Community component to use Iconify for icons and adjust CommunitiesSection configuration 2025-05-10 07:26:38 +07:00
ArraysID aba462e462 feat: add support for s3 bucket for media 2025-05-10 06:55:11 +07:00
ArraysID 55fd1fa443 refactor: move global section functions to services directory 2025-05-10 05:38:01 +07:00
ArraysID 07a1eb8dbc feat: add CommunitiesSection configuration and integrate with Community component 2025-05-10 05:35:24 +07:00
ArraysID aad04b6f94 fix: update revalidation interval to 10 seconds 2025-05-10 04:33:06 +07:00
ArraysID d4a91abb63 refactor: optimize data fetching by using Promise.all 2025-05-10 04:29:46 +07:00
ArraysID 7fd5528c0d feat: add revalidate constant to control page revalidation interval 2025-05-10 04:29:28 +07:00
ArraysID 39a57b4fed feat: add FeaturesSection global configuration and integrate with Features component 2025-05-10 04:08:37 +07:00
ArraysID 7b490702b9 feat: implement HeroSection global configuration and integrate with Hero component 2025-05-10 03:32:19 +07:00
ArraysID f31aad8570 fix: include withPayload in Next.js plugins for proper configuration 2025-05-08 16:07:51 +07:00
ArraysID acb7f4aef9 fix: update quotation marks for testimonials to use HTML entities 2025-05-08 15:42:24 +07:00
ArraysID c69614531b refactor: update config import paths to use local payload.config 2025-05-08 15:39:21 +07:00
ArraysID c82426282c fix: update TypeScript paths for payload configuration 2025-05-08 15:24:44 +07:00
ArraysID 45295953aa feat: add payload configuration and update TypeScript target 2025-05-08 15:16:59 +07:00
ArraysID 0561555276 feat: integrate payload cms to landing page 2025-05-08 15:12:32 +07:00
68 changed files with 379 additions and 776 deletions
@@ -1,5 +1,13 @@
# API Endpoint
VITE_API_URL=
# Payload CMS Secret Key (minimum 32 characters)
CMS_SECRET=your-very-long-secret-key
# PostgreSQL Connection String
CMS_POSTGRES_URL=postgres://user:password@host:port/database
# S3 Storage Configuration
CMS_STORAGE_ENDPOINT=https://your-s3-endpoint.com
CMS_STORAGE_ACCESS_KEY_ID=your-access-key-id
CMS_STORAGE_SECRET_ACCESS_KEY=your-secret-access-key
+4 -25
View File
@@ -6,10 +6,9 @@
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>.
2. **Backoffice** - Application for <a href="https://gacha.imphnen.dev/" target="_blank">Internal Management Website</a>.
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>.
1. **Gacha** - Application for <a href="https://gacha.imphnen.dev/" target="_blank">gacha website</a>.
2. **Backoffice** - Application for internal management.
3. **Dimentorin** - Application for mentoring services.
## How to install
@@ -25,15 +24,6 @@ This repository is a **monorepo** for all frontend services of IMPHNEN. The mono
## 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
Use the following commands to run in development mode:
@@ -50,10 +40,6 @@ Use the following commands to run in development mode:
```sh
npm run dimentorin:dev
```
- **Landing Page**:
```sh
npm run landing:dev
```
### Build
@@ -71,10 +57,6 @@ Use the following commands to build the applications:
```sh
npm run dimentorin:build
```
- **Landing Page**:
```sh
npm run landing:build
```
### Production
@@ -92,10 +74,6 @@ Use the following commands to run the applications in production mode:
```sh
npm run dimentorin:prod
```
- **Landing Page**:
```sh
npm run landing:prod
```
### Storybook
@@ -109,6 +87,7 @@ This repository uses Storybook to develop, test, and document UI components in a
npm run ui:storybook
```
- **Run Unit Test**
This command runs unit tests for the UI components to ensure they function as expected.
-1
View File
@@ -1 +0,0 @@
VITE_API_URL=
@@ -2,19 +2,24 @@ import { useForm } from 'react-hook-form';
import {
authLoginSchema,
TLoginRequest,
usePostLogin,
} from '@imphnen-frontend-service/service';
import { zodResolver } from '@hookform/resolvers/zod';
import { useSession } from '@imphnen-frontend-service/utils';
import { toast } from 'sonner';
export const useLogin = () => {
const postLogin = usePostLogin();
const form = useForm<TLoginRequest>({
resolver: zodResolver(authLoginSchema),
mode: 'all',
});
const { signIn } = useSession();
const onSubmit = form.handleSubmit((data) => signIn(data));
const onSubmit = form.handleSubmit((data) => {
postLogin.mutate(data, {
onSuccess: () => toast.success("Login sukses"),
onError: (error) => toast.error(error.message),
});
});
return {
form,
@@ -18,7 +18,7 @@ import {
RowSelectionState,
} from '@tanstack/react-table';
import ModalEditAccount from './_components/modal-edit-account';
import { useQueryState } from '@imphnen-frontend-service/utils';
import { useQueryState } from '../../hook/use-query-state';
interface Account {
id: number;
@@ -166,7 +166,7 @@ export const Components: FC = (): ReactElement => {
</Button>
{showFilter && (
<div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg">
<Filter onClose={() => setShowFilter(false)} options={[]} />
<Filter onClose={() => setShowFilter(false)} />
</div>
)}
</div>
@@ -0,0 +1,18 @@
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;
@@ -10,7 +10,7 @@ import { FC, Fragment, ReactElement, useState } from 'react';
import ModalAddItem from './_components/modal-add-item';
import ModalEditItem from './_components/modal-edit-item';
import ModalDeleteItem from './_components/modal-delete-item';
import { useQueryState } from '@imphnen-frontend-service/utils';
import { useQueryState } from '../../hook/use-query-state';
export const Components: FC = (): ReactElement => {
const [showModalAddItem, setShowModalAddItem] = useState(false);
@@ -0,0 +1,18 @@
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;
@@ -32,16 +32,7 @@ export const Components: FC = (): ReactElement => {
size="lg"
className="w-full"
/>
<Button
disabled={
form.formState.isSubmitting ||
form.formState.isValidating ||
!form.formState.isValid
}
type="submit"
size="md"
className="w-full"
>
<Button type="submit" size="md" className="w-full">
Login
</Button>
</form>
@@ -0,0 +1,18 @@
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
@@ -0,0 +1,18 @@
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
@@ -0,0 +1,18 @@
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;
@@ -0,0 +1,18 @@
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;
+3 -100
View File
@@ -1,108 +1,11 @@
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');
// };
import { LoaderFunctionArgs } from 'react-router-dom';
export const middleware = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url);
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 (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';
}
}
if (pathname) return null;
return null;
};
-1
View File
@@ -1 +0,0 @@
VITE_API_URL=
+9 -4
View File
@@ -2,19 +2,24 @@ import { useForm } from 'react-hook-form';
import {
authLoginSchema,
TLoginRequest,
usePostLogin,
} from '@imphnen-frontend-service/service';
import { zodResolver } from '@hookform/resolvers/zod';
import { useSession } from '@imphnen-frontend-service/utils';
import { toast } from 'sonner';
export const useLogin = () => {
const postLogin = usePostLogin();
const form = useForm<TLoginRequest>({
resolver: zodResolver(authLoginSchema),
mode: 'all',
});
const { signIn } = useSession();
const onSubmit = form.handleSubmit((data) => signIn(data));
const onSubmit = form.handleSubmit((data) => {
postLogin.mutate(data, {
onSuccess: () => toast.success("Login sukses"),
onError: (error) => toast.error(error.message),
});
});
return {
form,
+3 -101
View File
@@ -1,109 +1,11 @@
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');
// };
import { LoaderFunctionArgs } from 'react-router-dom';
export const middleware = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url);
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 (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';
}
}
if (pathname) return null;
return null;
};
-1
View File
@@ -1 +0,0 @@
VITE_API_URL=
+7 -4
View File
@@ -2,19 +2,22 @@ import { useForm } from 'react-hook-form';
import {
authLoginSchema,
TLoginRequest,
usePostLogin,
} from '@imphnen-frontend-service/service';
import { zodResolver } from '@hookform/resolvers/zod';
import { useSession } from '@imphnen-frontend-service/utils';
export const useLogin = () => {
const postLogin = usePostLogin();
const form = useForm<TLoginRequest>({
resolver: zodResolver(authLoginSchema),
mode: 'all',
});
const { signIn } = useSession();
const onSubmit = form.handleSubmit((data) => signIn(data));
const onSubmit = form.handleSubmit((data) => {
postLogin.mutate(data, {
onSuccess: () => console.log('Success Login'),
});
});
return {
form,
+24 -1
View File
@@ -1,11 +1,14 @@
import { useForm } from 'react-hook-form';
import {
authRegisterSchema,
stepOneRegisterSchema,
TRegisterRequest,
usePostRegister,
} from '@imphnen-frontend-service/service';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { toast } from 'sonner';
import { useEffect, useState } from 'react';
export const useRegister = () => {
const postRegister = usePostRegister();
@@ -18,12 +21,31 @@ export const useRegister = () => {
password: '',
confirm_password: '',
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) => {
postRegister.mutate(data, {
onSuccess: () => toast.success('Registrasi sukses'),
onSuccess: (data) => toast.success(data.message),
onError: (error) => toast.error(error.message),
});
});
@@ -31,5 +53,6 @@ export const useRegister = () => {
return {
form,
onSubmit,
isStepOneValid: stepValid,
};
};
+3 -101
View File
@@ -1,109 +1,11 @@
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');
// };
import { LoaderFunctionArgs } from 'react-router-dom';
export const middleware = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url);
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 (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';
}
}
if (pathname) return null;
return null;
};
@@ -69,7 +69,7 @@ export function Hero(props: HeroSection) {
</div>
<div className="space-y-4">
<h1 className="text-4xl md:text-5xl lg:text-6xl xl:text-7xl font-bold tracking-tighter bg-clip-text text-transparent bg-gradient-to-r from-foreground via-foreground to-foreground/70">
<h1 className="text-4xl font-bold tracking-tighter sm:text-5xl md:text-6xl lg:text-7xl bg-clip-text text-transparent bg-gradient-to-r from-foreground via-foreground to-foreground/70">
{title} <br />
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
{highlight}
@@ -80,10 +80,10 @@ export function Hero(props: HeroSection) {
</p>
</div>
<div className="flex flex-col sm:flex-row gap-4 w-full sm:w-auto">
<div className="flex flex-col sm:flex-row gap-4">
<Button
size="lg"
className="w-full sm:w-auto bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90 transition-all duration-300 font-bold text-black hover:text-white cursor-pointer"
className="bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90 transition-all duration-300 font-bold text-black hover:text-white cursor-pointer"
onClick={() => window.open(buttons.primaryUrl, '_blank')}
>
{buttons.primaryLabel}
@@ -91,7 +91,7 @@ export function Hero(props: HeroSection) {
<Button
size="lg"
variant="outline"
className="w-full sm:w-auto group relative overflow-hidden border-primary"
className="group relative overflow-hidden border-primary"
onClick={() => window.open(buttons.secondaryUrl, '_blank')}
>
<span className="absolute inset-0 bg-gradient-to-r from-primary/10 to-blue-400/10 translate-y-full group-hover:translate-y-0 transition-transform duration-300" />
@@ -99,7 +99,7 @@ export function Hero(props: HeroSection) {
</Button>
</div>
<div className="flex flex-wrap justify-center gap-6 sm:gap-8">
<div className="flex items-center gap-8">
{stats?.map(({ value, label }, i) => (
<Fragment key={i}>
<div className="flex flex-col items-center">
@@ -109,7 +109,7 @@ export function Hero(props: HeroSection) {
<div className="text-xs text-muted-foreground">{label}</div>
</div>
{i < stats.length - 1 && (
<div className="hidden sm:block h-10 border-r border-border mx-4" />
<div className="h-10 border-r border-border mx-4" />
)}
</Fragment>
))}
@@ -117,7 +117,7 @@ export function Hero(props: HeroSection) {
</motion.div>
<motion.div
className="relative w-full lg:w-auto mx-auto lg:ml-auto"
className="relative mx-auto lg:ml-auto"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 0.2 }}
+6
View File
@@ -6,6 +6,7 @@ import {
TRegisterRequest,
TVerifyEmailRequest,
} from '../../types/auth';
import { SessionToken, SessionUser } from '@imphnen-frontend-service/utils';
import { TResponseError, TResponseMessage } from '../../types/common';
@@ -18,6 +19,11 @@ export const usePostLogin = (): UseMutationResult<
return useMutation({
mutationKey: ['post-login'],
mutationFn: async (payload) => await postLogin(payload),
onSuccess: (res) => {
SessionUser.set(res.data.user);
SessionToken.set(res.data.token);
window.location.reload();
},
});
};
+26 -7
View File
@@ -16,7 +16,7 @@ export const authLoginSchema = z.object({
.min(1, 'Password tidak boleh kosong'),
});
export const authRegisterSchema = z
export const stepOneRegisterSchema = z
.object({
email: z
.string({
@@ -25,12 +25,6 @@ export const authRegisterSchema = z
})
.min(1, 'Email tidak boleh kosong')
.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
.string({
required_error: 'Nama tidak boleh kosong',
@@ -57,3 +51,28 @@ export const authRegisterSchema = z
message: 'Password dan Konfirmasi Password harus sama',
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
);

Some files were not shown because too many files have changed in this diff Show More