feat: login example
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import {
|
import {
|
||||||
InputForm,
|
InputField,
|
||||||
Modal,
|
Modal,
|
||||||
Stepper,
|
Stepper,
|
||||||
} from '@imphnen-frontend-service/ui/molecules';
|
} from '@imphnen-frontend-service/ui/molecules';
|
||||||
import { useQueryState } from '../../../hooks/use-query-state';
|
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||||
|
import { Fragment } from 'react/jsx-runtime';
|
||||||
interface IModalFormForgotPasswordProps {
|
interface IModalFormForgotPasswordProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -62,7 +62,7 @@ interface IStepOneProps {
|
|||||||
|
|
||||||
const StepOne = ({ nextStep, onClose }: IStepOneProps) => (
|
const StepOne = ({ nextStep, onClose }: IStepOneProps) => (
|
||||||
<>
|
<>
|
||||||
<InputForm
|
<InputField
|
||||||
label="Email"
|
label="Email"
|
||||||
placeholder="Masukkan Email yang Terdaftar"
|
placeholder="Masukkan Email yang Terdaftar"
|
||||||
type="email"
|
type="email"
|
||||||
@@ -86,9 +86,8 @@ interface IStepTwoProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const StepTwo = ({ nextStep, prevStep }: IStepTwoProps) => (
|
const StepTwo = ({ nextStep, prevStep }: IStepTwoProps) => (
|
||||||
<>
|
<Fragment>
|
||||||
{/* TODO: Change component using OTP Input */}
|
<InputField
|
||||||
<InputForm
|
|
||||||
label="Kode OTP"
|
label="Kode OTP"
|
||||||
placeholder="Masukkan Kode OTP"
|
placeholder="Masukkan Kode OTP"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -108,7 +107,7 @@ const StepTwo = ({ nextStep, prevStep }: IStepTwoProps) => (
|
|||||||
Reset Password
|
Reset Password
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|
||||||
interface IStepThreeProps {
|
interface IStepThreeProps {
|
||||||
@@ -118,14 +117,14 @@ interface IStepThreeProps {
|
|||||||
|
|
||||||
const StepThree = ({ onClose, resetStep }: IStepThreeProps) => (
|
const StepThree = ({ onClose, resetStep }: IStepThreeProps) => (
|
||||||
<>
|
<>
|
||||||
<InputForm
|
<InputField
|
||||||
label="Password Baru"
|
label="Password Baru"
|
||||||
placeholder="Masukkan Password Baru"
|
placeholder="Masukkan Password Baru"
|
||||||
type="password"
|
type="password"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
<InputForm
|
<InputField
|
||||||
label="Ulang Password"
|
label="Ulang Password"
|
||||||
placeholder="Masukkan Ulang Password"
|
placeholder="Masukkan Ulang Password"
|
||||||
type="password"
|
type="password"
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import { InputForm, Modal } from '@imphnen-frontend-service/ui/molecules';
|
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||||
|
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useLogin } from '../../_hooks/use-login';
|
||||||
|
|
||||||
interface IModalFormLogin {
|
interface IModalFormLogin {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -13,6 +15,8 @@ const ModalFormLogin = ({
|
|||||||
onClose,
|
onClose,
|
||||||
onForgotPassword,
|
onForgotPassword,
|
||||||
}: IModalFormLogin) => {
|
}: IModalFormLogin) => {
|
||||||
|
const { form, onSubmit } = useLogin();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7"
|
className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7"
|
||||||
@@ -25,38 +29,44 @@ const ModalFormLogin = ({
|
|||||||
Login
|
Login
|
||||||
</h1>
|
</h1>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Content className="space-y-4">
|
<Modal.Content>
|
||||||
<InputForm
|
<form className="space-y-4" onSubmit={onSubmit}>
|
||||||
label="Email"
|
<ControlledInputField
|
||||||
placeholder="Masukkan Email"
|
control={form.control}
|
||||||
type="email"
|
label="Email"
|
||||||
size="lg"
|
placeholder="Masukkan Email"
|
||||||
className="w-full"
|
type="email"
|
||||||
/>
|
name="email"
|
||||||
<InputForm
|
size="lg"
|
||||||
label="Password"
|
className="w-full"
|
||||||
placeholder="Masukkan Password"
|
/>
|
||||||
type="password"
|
<ControlledInputField
|
||||||
size="lg"
|
control={form.control}
|
||||||
className="w-full"
|
label="Password"
|
||||||
/>
|
placeholder="Masukkan Password"
|
||||||
<h1 className="text-end text-primary-500 text-xl font-medium">
|
type="password"
|
||||||
<Button variant="text" onClick={onForgotPassword}>
|
name="password"
|
||||||
Lupa Password?
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<h1 className="text-end text-primary-500 text-xl font-medium">
|
||||||
|
<Button variant="text" onClick={onForgotPassword}>
|
||||||
|
Lupa Password?
|
||||||
|
</Button>
|
||||||
|
</h1>
|
||||||
|
<Button type="submit" size="md" className="w-full">
|
||||||
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
</h1>
|
<div className="flex justify-center gap-2 pt-2.5 font-medium">
|
||||||
<Button size="md" className="w-full">
|
<p className="text-neutral-500">Belum punya akun?</p>
|
||||||
Login
|
<Link
|
||||||
</Button>
|
className="text-primary-500 hover:text-primary-600"
|
||||||
<div className="flex justify-center gap-2 pt-2.5 font-medium">
|
to="/register"
|
||||||
<p className="text-neutral-500">Belum punya akun?</p>
|
>
|
||||||
<Link
|
Daftar
|
||||||
className="text-primary-500 hover:text-primary-600"
|
</Link>
|
||||||
to="/register"
|
</div>
|
||||||
>
|
</form>
|
||||||
Daftar
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</Modal.Content>
|
</Modal.Content>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import { InputForm, Modal } from '@imphnen-frontend-service/ui/molecules';
|
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||||
import { useQueryState } from '../../../hooks/use-query-state';
|
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||||
|
|
||||||
interface IModalFormRegisterProps {
|
interface IModalFormRegisterProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -54,28 +54,28 @@ interface IStepOneProps {
|
|||||||
|
|
||||||
const StepOne = ({ nextStep, onClose }: IStepOneProps) => (
|
const StepOne = ({ nextStep, onClose }: IStepOneProps) => (
|
||||||
<>
|
<>
|
||||||
<InputForm
|
<InputField
|
||||||
label="Nama Lengkap"
|
label="Nama Lengkap"
|
||||||
placeholder="Masukkan Nama Lengkap"
|
placeholder="Masukkan Nama Lengkap"
|
||||||
type="text"
|
type="text"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
<InputForm
|
<InputField
|
||||||
label="Email"
|
label="Email"
|
||||||
placeholder="Masukkan Email Anda"
|
placeholder="Masukkan Email Anda"
|
||||||
type="email"
|
type="email"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
<InputForm
|
<InputField
|
||||||
label="Password"
|
label="Password"
|
||||||
placeholder="Masukkan Password"
|
placeholder="Masukkan Password"
|
||||||
type="password"
|
type="password"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
<InputForm
|
<InputField
|
||||||
label="Ulangi Password"
|
label="Ulangi Password"
|
||||||
placeholder="Masukkan Ulang Password"
|
placeholder="Masukkan Ulang Password"
|
||||||
type="password"
|
type="password"
|
||||||
@@ -95,14 +95,14 @@ interface IStepTwoProps {
|
|||||||
|
|
||||||
const StepTwo = ({ nextStep, prevStep }: IStepTwoProps) => (
|
const StepTwo = ({ nextStep, prevStep }: IStepTwoProps) => (
|
||||||
<>
|
<>
|
||||||
<InputForm
|
<InputField
|
||||||
label="Nomor Telepon"
|
label="Nomor Telepon"
|
||||||
placeholder="Masukkan Nomor Telepon Aktif"
|
placeholder="Masukkan Nomor Telepon Aktif"
|
||||||
type="text"
|
type="text"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
<InputForm
|
<InputField
|
||||||
label="Alamat Pengiriman"
|
label="Alamat Pengiriman"
|
||||||
placeholder="Masukkan Alamat Pengiriman"
|
placeholder="Masukkan Alamat Pengiriman"
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { cn } from '@imphnen-frontend-service/utils';
|
||||||
|
import { FC, ReactElement } from 'react';
|
||||||
|
|
||||||
|
type TGachaItem = {
|
||||||
|
src: string;
|
||||||
|
label: string;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GachaItem: FC<TGachaItem> = ({
|
||||||
|
src,
|
||||||
|
label,
|
||||||
|
className,
|
||||||
|
}): ReactElement => {
|
||||||
|
return (
|
||||||
|
<div className="snap-center flex-auto justify-center items-center flex flex-col min-w-full overflow-hidden">
|
||||||
|
<div className="max-h-[180px] md:max-h-[270px] md:max-w-[500px] mb-2 md:mb-5 flex flex-1 justify-center items-center">
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt="Banner"
|
||||||
|
width={255}
|
||||||
|
height={255}
|
||||||
|
className={cn('h-[150px] md:h-[255px] object-contain', className)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="font-semibold text-center md:text-p2">{label}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import {
|
||||||
|
authLoginSchema,
|
||||||
|
TLoginRequest,
|
||||||
|
usePostLogin,
|
||||||
|
} from '@imphnen-frontend-service/service';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|
||||||
|
export const useLogin = () => {
|
||||||
|
const postLogin = usePostLogin();
|
||||||
|
const form = useForm<TLoginRequest>({
|
||||||
|
resolver: zodResolver(authLoginSchema),
|
||||||
|
mode: 'all',
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = form.handleSubmit((data) => {
|
||||||
|
postLogin.mutate(data, {
|
||||||
|
onSuccess: () => console.log('Success Login'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
onSubmit,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,37 +1,10 @@
|
|||||||
import { ArrowDownOutlined } from '@ant-design/icons';
|
import { ArrowDownOutlined } from '@ant-design/icons';
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import { cn } from '@imphnen-frontend-service/utils';
|
|
||||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
import { FC, Fragment, ReactElement, useState } from 'react';
|
||||||
import ModalFormForgotPassword from './_components/form/modal-form-forgot-password';
|
import ModalFormForgotPassword from './_components/form/modal-form-forgot-password';
|
||||||
import ModalFormLogin from './_components/form/modal-form-login';
|
import ModalFormLogin from './_components/form/modal-form-login';
|
||||||
import ModalFormRegister from './_components/form/modal-form-register';
|
import ModalFormRegister from './_components/form/modal-form-register';
|
||||||
|
import { GachaItem } from './_components/item/gacha-item';
|
||||||
interface GachaItemProps {
|
|
||||||
src: string;
|
|
||||||
label: string;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const GachaItem: FC<GachaItemProps> = ({
|
|
||||||
src,
|
|
||||||
label,
|
|
||||||
className,
|
|
||||||
}): ReactElement => {
|
|
||||||
return (
|
|
||||||
<div className="snap-center flex-auto justify-center items-center flex flex-col min-w-full overflow-hidden">
|
|
||||||
<div className="max-h-[180px] md:max-h-[270px] md:max-w-[500px] mb-2 md:mb-5 flex flex-1 justify-center items-center">
|
|
||||||
<img
|
|
||||||
src={src}
|
|
||||||
alt="Banner"
|
|
||||||
width={255}
|
|
||||||
height={255}
|
|
||||||
className={cn('h-[150px] md:h-[255px] object-contain', className)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p className="font-semibold text-center md:text-p2">{label}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
export const Components: FC = (): ReactElement => {
|
||||||
const [showModalForgotPassword, setShowModalForgotPassword] = useState(false);
|
const [showModalForgotPassword, setShowModalForgotPassword] = useState(false);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { api } from '@imphnen-frontend-service/utils';
|
import { api } from '../';
|
||||||
import {
|
import {
|
||||||
TLoginRequest,
|
TLoginRequest,
|
||||||
TLoginResponse,
|
TLoginResponse,
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
import axios, { AxiosRequestConfig } from 'axios';
|
||||||
|
|
||||||
export * from './auth';
|
export * from './auth';
|
||||||
export * from './gacha';
|
export * from './gacha';
|
||||||
export * from './users';
|
export * from './users';
|
||||||
|
|
||||||
|
const config: AxiosRequestConfig = {
|
||||||
|
baseURL: import.meta.env.VITE_API_URL,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const api = axios.create(config);
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ 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';
|
||||||
|
|
||||||
export const usePostLogin = (): UseMutationResult<
|
export const usePostLogin = (): UseMutationResult<
|
||||||
@@ -17,6 +19,11 @@ 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();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './api';
|
export * from './api';
|
||||||
export * from './hooks';
|
export * from './hooks';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
export * from './schemas';
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const authLoginSchema = z.object({
|
||||||
|
email: z
|
||||||
|
.string()
|
||||||
|
.min(1, 'Email cannot be empty')
|
||||||
|
.email('Email must be valid'),
|
||||||
|
password: z.string().min(1, 'Password cannot be empty'),
|
||||||
|
});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './auth';
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { TUserItem } from '../users';
|
||||||
|
|
||||||
export type TLoginRequest = {
|
export type TLoginRequest = {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
@@ -9,11 +11,7 @@ export type TLoginResponse = {
|
|||||||
access_token: string;
|
access_token: string;
|
||||||
refresh_token: string;
|
refresh_token: string;
|
||||||
};
|
};
|
||||||
user: {
|
user: TUserItem;
|
||||||
fullname: string;
|
|
||||||
email: string;
|
|
||||||
is_active: boolean;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
export * from './auth';
|
export * from './auth';
|
||||||
export * from './gacha';
|
export * from './gacha';
|
||||||
export * from './users';
|
export * from './users';
|
||||||
|
export * from './roles';
|
||||||
|
export * from './permissions';
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export type TPermissionItem = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { TPermissionItem } from '../permissions';
|
||||||
|
|
||||||
|
export type TRoleItem = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
permissions: TPermissionItem[];
|
||||||
|
};
|
||||||
@@ -1 +1,19 @@
|
|||||||
export {};
|
import { TRoleItem } from '../roles';
|
||||||
|
|
||||||
|
export type TUserItem = {
|
||||||
|
id: string;
|
||||||
|
avatar: string;
|
||||||
|
birthdate: string;
|
||||||
|
email: string;
|
||||||
|
fullname: string;
|
||||||
|
gender: string;
|
||||||
|
identity_number: string;
|
||||||
|
is_active: boolean;
|
||||||
|
is_profile_completed: boolean;
|
||||||
|
phone_number: string;
|
||||||
|
referral_code: string;
|
||||||
|
referred_by: string;
|
||||||
|
religion: string;
|
||||||
|
student_type: string;
|
||||||
|
role: TRoleItem;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export * from './forgot-step';
|
export * from './forgot-step';
|
||||||
export * from './otp-form';
|
export * from './otp-form';
|
||||||
export * from './input-form';
|
export * from './input-field';
|
||||||
export * from './pagination';
|
export * from './pagination';
|
||||||
export * from './modal/modal';
|
export * from './modal/modal';
|
||||||
export * from './stepper';
|
export * from './stepper';
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './input-field';
|
||||||
+3
-3
@@ -1,9 +1,9 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import InputForm from './input-form';
|
import { InputField } from './input-field';
|
||||||
|
|
||||||
describe('InputForm Component', () => {
|
describe('InputForm Component', () => {
|
||||||
it('renders correctly with disabled prop', () => {
|
it('renders correctly with disabled prop', () => {
|
||||||
render(<InputForm label="Test Label" disabled={true} />);
|
render(<InputField label="Test Label" disabled={true} />);
|
||||||
|
|
||||||
const input = screen.getByLabelText('Test Label');
|
const input = screen.getByLabelText('Test Label');
|
||||||
expect(input).toBeDisabled();
|
expect(input).toBeDisabled();
|
||||||
@@ -11,7 +11,7 @@ describe('InputForm Component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders correctly without disabled prop', () => {
|
it('renders correctly without disabled prop', () => {
|
||||||
render(<InputForm label="Test Label" disabled={false} />);
|
render(<InputField label="Test Label" disabled={false} />);
|
||||||
|
|
||||||
const input = screen.getByLabelText('Test Label');
|
const input = screen.getByLabelText('Test Label');
|
||||||
expect(input).not.toBeDisabled();
|
expect(input).not.toBeDisabled();
|
||||||
+3
-3
@@ -1,9 +1,9 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react';
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
import { InputForm } from './input-form';
|
import { InputField } from './input-field';
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Molecules/Input Form',
|
title: 'Molecules/Input Form',
|
||||||
component: InputForm,
|
component: InputField,
|
||||||
parameters: {
|
parameters: {
|
||||||
layout: 'centered',
|
layout: 'centered',
|
||||||
docs: {
|
docs: {
|
||||||
@@ -27,7 +27,7 @@ Cek dan inspect element pada story With HtmlFor untuk melihat hasilnya.
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
} satisfies Meta<typeof InputForm>;
|
} satisfies Meta<typeof InputField>;
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof meta>;
|
type Story = StoryObj<typeof meta>;
|
||||||
+4
-8
@@ -7,10 +7,9 @@ import {
|
|||||||
import { Input } from '../../atoms';
|
import { Input } from '../../atoms';
|
||||||
import { cn } from '@imphnen-frontend-service/utils';
|
import { cn } from '@imphnen-frontend-service/utils';
|
||||||
|
|
||||||
type TInputType = 'text' | 'email' | 'password' | 'file';
|
export type TInputType = 'text' | 'email' | 'password' | 'file';
|
||||||
type TInputSize = 'sm' | 'md' | 'lg';
|
export type TInputSize = 'sm' | 'md' | 'lg';
|
||||||
|
export type TInputFieldProps = Omit<
|
||||||
type TInputFormProps = Omit<
|
|
||||||
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
|
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
|
||||||
'size' | 'type'
|
'size' | 'type'
|
||||||
> & {
|
> & {
|
||||||
@@ -19,7 +18,6 @@ type TInputFormProps = Omit<
|
|||||||
size?: TInputSize;
|
size?: TInputSize;
|
||||||
error?: string;
|
error?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
|
||||||
helperText?: string;
|
helperText?: string;
|
||||||
htmlFor?: string;
|
htmlFor?: string;
|
||||||
};
|
};
|
||||||
@@ -39,7 +37,7 @@ const sizeClasses: Record<TInputSize, { label: string; helperText: string }> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InputForm: FC<TInputFormProps> = ({
|
export const InputField: FC<TInputFieldProps> = ({
|
||||||
label,
|
label,
|
||||||
placeholder,
|
placeholder,
|
||||||
type = 'text',
|
type = 'text',
|
||||||
@@ -88,5 +86,3 @@ export const InputForm: FC<TInputFormProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InputForm;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './input-form';
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import {
|
||||||
|
InputField,
|
||||||
|
TInputFieldProps,
|
||||||
|
} from '@imphnen-frontend-service/ui/molecules';
|
||||||
|
import {
|
||||||
|
FieldValues,
|
||||||
|
useController,
|
||||||
|
UseControllerProps,
|
||||||
|
} from 'react-hook-form';
|
||||||
|
|
||||||
|
export type TControlledInputFieldProps<T extends FieldValues> =
|
||||||
|
UseControllerProps<T> & TInputFieldProps;
|
||||||
|
|
||||||
|
export const ControlledInputField = <T extends FieldValues>(
|
||||||
|
props: TControlledInputFieldProps<T>
|
||||||
|
) => {
|
||||||
|
const { field, fieldState } = useController<T>(props);
|
||||||
|
return (
|
||||||
|
<InputField error={fieldState.error?.message} {...{ ...props, ...field }} />
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './controlled-input-field';
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export * from './navbar';
|
export * from './navbar';
|
||||||
export * from "./modals-gacha";
|
export * from './modals-gacha';
|
||||||
export * from "./auth-banner";
|
export * from './auth-banner';
|
||||||
export * from './backoffice-sidebar'
|
export * from './backoffice-sidebar';
|
||||||
export * from './datatable'
|
export * from './datatable';
|
||||||
export * from './filter'
|
export * from './filter';
|
||||||
|
export * from './controlled-field';
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ describe('Navbar', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has correct ARIA role for navigation', () => {
|
it('has correct ARIA role for nav', () => {
|
||||||
const { container }: RenderResult = render(
|
const { container }: RenderResult = render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
@@ -171,6 +171,6 @@ describe('Navbar', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const header: HTMLElement | null = container.querySelector('header');
|
const header: HTMLElement | null = container.querySelector('header');
|
||||||
expect(header).toHaveAttribute('role', 'navigation');
|
expect(header).toHaveAttribute('role', 'nav');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import { MenuOutlined } from '@ant-design/icons';
|
import { MenuOutlined } from '@ant-design/icons';
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { FC, ReactElement, useState } from 'react';
|
import { FC, ReactElement, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Button } from '../../atoms/button';
|
||||||
|
import { useSession } from '@imphnen-frontend-service/utils';
|
||||||
|
|
||||||
export const Navbar: FC = (): ReactElement => {
|
export const Navbar: FC = (): ReactElement => {
|
||||||
const [isDropdownOpen, setDropdownOpen] = useState(false);
|
const { session, signOut, isAuthenticated } = useSession();
|
||||||
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-primary-50 w-full px-[32px] pt-[32px] md:px-[60px] md:pt-[60px] lg:px-[80px] sticky top-0 z-50">
|
<div className="bg-primary-50 w-full px-[32px] pt-[32px] md:px-[60px] md:pt-[60px] lg:px-[80px] sticky top-0 z-50">
|
||||||
<header
|
<header
|
||||||
className="bg-white shadow-lg rounded-lg min-h-[47px] max-h-[47px] md:min-h-[60px] md:max-h-[60px] lg:min-h-[71px] lg:max-h-[71px] flex justify-between w-full max-w-[1280px] xl:mx-auto"
|
className="bg-white shadow-lg rounded-lg min-h-[47px] max-h-[47px] md:min-h-[60px] md:max-h-[60px] lg:min-h-[71px] lg:max-h-[71px] flex justify-between w-full max-w-[1280px] xl:mx-auto"
|
||||||
role="navigation"
|
role="nav"
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-between p-4 md:px-[32px] md:py-[10px]">
|
<div className="flex w-full items-center justify-between p-4 md:px-[32px] md:py-[10px]">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@@ -40,20 +42,27 @@ export const Navbar: FC = (): ReactElement => {
|
|||||||
<Link to="#">Merch Gacha</Link>
|
<Link to="#">Merch Gacha</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
{!isAuthenticated ? (
|
||||||
<Button
|
<li>
|
||||||
size="md"
|
<Button>Login</Button>
|
||||||
className="lg:text-[19px] lg:max-h-[44px] text-neutral-50 hover:text-neutral-200 transition-colors"
|
</li>
|
||||||
>
|
) : (
|
||||||
<Link to="/login">Login</Link>
|
<li className="flex gap-x-4">
|
||||||
</Button>
|
<span className="text-lg">{session.user?.fullname}</span>
|
||||||
</li>
|
<div
|
||||||
|
onClick={signOut}
|
||||||
|
className="text-lg text-red-500 font-bold"
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
<button
|
<button
|
||||||
className={`md:hidden duration-200 ${
|
className={`md:hidden duration-200 ${
|
||||||
isDropdownOpen ? 'transform rotate-90' : ''
|
isDropdownOpen ? 'transform rotate-90' : ''
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setDropdownOpen(!isDropdownOpen)}
|
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
||||||
>
|
>
|
||||||
<MenuOutlined style={{ color: '#1a8ce6' }} />
|
<MenuOutlined style={{ color: '#1a8ce6' }} />
|
||||||
</button>
|
</button>
|
||||||
@@ -77,14 +86,18 @@ export const Navbar: FC = (): ReactElement => {
|
|||||||
Merch Gacha
|
Merch Gacha
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
{!isAuthenticated ? (
|
||||||
<Link
|
<li>
|
||||||
to="#"
|
<Link
|
||||||
className="block text-gray-600 transition-colors px-4 py-2 text-center font-semibold"
|
to="#"
|
||||||
>
|
className="block text-gray-600 transition-colors px-4 py-2 text-center font-semibold"
|
||||||
Login
|
>
|
||||||
</Link>
|
Login
|
||||||
</li>
|
</Link>
|
||||||
|
</li>
|
||||||
|
) : (
|
||||||
|
<li>{session.user?.fullname}</li>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import axios, { AxiosRequestConfig } from 'axios';
|
|
||||||
|
|
||||||
const config: AxiosRequestConfig = {
|
|
||||||
baseURL: import.meta.env.VITE_API_URL,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const api = axios.create(config);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './api';
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './use-query-state';
|
||||||
|
export * from './use-session';
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { SessionToken, SessionUser } from '../local-storage';
|
||||||
|
|
||||||
|
export const useSession = () => {
|
||||||
|
const session = {
|
||||||
|
user: SessionUser.get(),
|
||||||
|
token: SessionToken.get(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const isAuthenticated = !!session.token?.access_token;
|
||||||
|
|
||||||
|
const signOut = () => {
|
||||||
|
SessionUser.remove();
|
||||||
|
SessionToken.remove();
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAuthenticated,
|
||||||
|
session,
|
||||||
|
signOut,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './react-query';
|
export * from './react-query';
|
||||||
export * from './react-router';
|
export * from './react-router';
|
||||||
export * from './tailwind-merge';
|
export * from './tailwind-merge';
|
||||||
export * from './axios';
|
export * from './hooks';
|
||||||
|
export * from './local-storage';
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
export type TPermissionItem = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TRoleItem = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
permissions: TPermissionItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type TUserItem = {
|
||||||
|
id: string;
|
||||||
|
avatar: string;
|
||||||
|
birthdate: string;
|
||||||
|
email: string;
|
||||||
|
fullname: string;
|
||||||
|
gender: string;
|
||||||
|
identity_number: string;
|
||||||
|
is_active: boolean;
|
||||||
|
is_profile_completed: boolean;
|
||||||
|
phone_number: string;
|
||||||
|
referral_code: string;
|
||||||
|
referred_by: string;
|
||||||
|
religion: string;
|
||||||
|
student_type: string;
|
||||||
|
role: TRoleItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SessionUser = {
|
||||||
|
set: (val: TUserItem) => localStorage.setItem('users', JSON.stringify(val)),
|
||||||
|
get: (): TUserItem | undefined => {
|
||||||
|
const users = localStorage.getItem('users');
|
||||||
|
return users ? JSON.parse(users) : undefined;
|
||||||
|
},
|
||||||
|
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');
|
||||||
|
},
|
||||||
|
};
|
||||||
Generated
+591
-616
File diff suppressed because it is too large
Load Diff
+8
-2
@@ -19,14 +19,19 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^5.6.1",
|
"@ant-design/icons": "^5.6.1",
|
||||||
|
"@hookform/resolvers": "^5.0.1",
|
||||||
"@tanstack/react-query": "^5.67.3",
|
"@tanstack/react-query": "^5.67.3",
|
||||||
|
"@tanstack/react-store": "^0.7.0",
|
||||||
"@tanstack/react-table": "^8.21.2",
|
"@tanstack/react-table": "^8.21.2",
|
||||||
"axios": "^1.8.3",
|
"axios": "^1.8.3",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-hook-form": "^7.55.0",
|
||||||
"react-router-dom": "^7.3.0",
|
"react-router-dom": "^7.3.0",
|
||||||
"tailwind-merge": "^3.0.2"
|
"tailwind-merge": "^3.0.2",
|
||||||
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.14.5",
|
"@babel/core": "^7.14.5",
|
||||||
@@ -48,7 +53,7 @@
|
|||||||
"@storybook/test-runner": "^0.19.0",
|
"@storybook/test-runner": "^0.19.0",
|
||||||
"@storybook/testing-library": "^0.2.2",
|
"@storybook/testing-library": "^0.2.2",
|
||||||
"@swc-node/register": "~1.9.1",
|
"@swc-node/register": "~1.9.1",
|
||||||
"@swc/cli": "~0.3.12",
|
"@swc/cli": "^0.6.0",
|
||||||
"@swc/core": "~1.5.7",
|
"@swc/core": "~1.5.7",
|
||||||
"@swc/helpers": "~0.5.11",
|
"@swc/helpers": "~0.5.11",
|
||||||
"@tailwindcss/postcss": "^4.0.13",
|
"@tailwindcss/postcss": "^4.0.13",
|
||||||
@@ -56,6 +61,7 @@
|
|||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.1.0",
|
"@testing-library/react": "^16.1.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/node": "18.16.9",
|
"@types/node": "18.16.9",
|
||||||
"@types/react": "19.0.0",
|
"@types/react": "19.0.0",
|
||||||
"@types/react-dom": "19.0.0",
|
"@types/react-dom": "19.0.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user