Compare commits
95
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a84e2fc31a | ||
|
|
6e1c561387 | ||
|
|
b39e918b3c | ||
|
|
0a217341a5 | ||
|
|
f1f72402b2 | ||
|
|
3f1b2a2bd7 | ||
|
|
57b27c6784 | ||
|
|
f191ffc4b1 | ||
|
|
31e4ffae92 | ||
|
|
c9f7ce24ed | ||
|
|
d21adebf73 | ||
|
|
744d39ea75 | ||
|
|
da11ba5894 | ||
|
|
6d29c81dad | ||
|
|
4a7135dd17 | ||
|
|
33d750b221 | ||
|
|
1f9aa2e6b3 | ||
|
|
7722c98c84 | ||
|
|
0914361635 | ||
|
|
74aabd1eba | ||
|
|
d1fc7a8fd5 | ||
|
|
009cabfc1c | ||
|
|
a7f7521761 | ||
|
|
54b7a2f0eb | ||
|
|
ef042f2f40 | ||
|
|
fd35cb9525 | ||
|
|
18d11fd1c3 | ||
|
|
c82d9bcc70 | ||
|
|
a4dd793bb7 | ||
|
|
bba3c89b23 | ||
|
|
8b6aa21f82 | ||
|
|
0fffe2d403 | ||
|
|
246adb57a1 | ||
|
|
75839271ad | ||
|
|
919fac811d | ||
|
|
4316dcf8b3 | ||
|
|
b9b71c4c25 | ||
|
|
121faf89c3 | ||
|
|
07913efb16 | ||
|
|
4b2733511c | ||
|
|
9f3d00a0db | ||
|
|
7ac571c019 | ||
|
|
a8c958400d | ||
|
|
760ab67ae0 | ||
|
|
c4cf9e4e6c | ||
|
|
a800b1e293 | ||
|
|
da0e599b9a | ||
|
|
a02a259f4e | ||
|
|
ff99564b05 | ||
|
|
13ab089afc | ||
|
|
378e258520 | ||
|
|
88468081b6 | ||
|
|
aba462e462 | ||
|
|
55fd1fa443 | ||
|
|
07a1eb8dbc | ||
|
|
aad04b6f94 | ||
|
|
d4a91abb63 | ||
|
|
7fd5528c0d | ||
|
|
39a57b4fed | ||
|
|
7b490702b9 | ||
|
|
6e78b49731 | ||
|
|
f31aad8570 | ||
|
|
acb7f4aef9 | ||
|
|
c69614531b | ||
|
|
c82426282c | ||
|
|
45295953aa | ||
|
|
0561555276 | ||
|
|
3aa552afd6 | ||
|
|
b389235971 | ||
|
|
86290cc820 | ||
|
|
41712be56a | ||
|
|
240e684b53 | ||
|
|
b1a53df4a9 | ||
|
|
0709c3a2bd | ||
|
|
f2e1cead83 | ||
|
|
1b795f2dfc | ||
|
|
ce115f63e4 | ||
|
|
c907c696f5 | ||
|
|
de76020709 | ||
|
|
1e391a2932 | ||
|
|
a7c67cb77f | ||
|
|
dafdfba490 | ||
|
|
a741fbb8d4 | ||
|
|
0e3dc101d5 | ||
|
|
e805e5fd41 | ||
|
|
5bdf8f5a4d | ||
|
|
07f1fb0208 | ||
|
|
3958ae1098 | ||
|
|
885d156833 | ||
|
|
2746af0b3f | ||
|
|
745a2d2873 | ||
|
|
8887e3b0a9 | ||
|
|
638b64b715 | ||
|
|
d61cb4a215 | ||
|
|
272db82c8c |
+14
-1
@@ -1 +1,14 @@
|
||||
VITE_API_URL=https://api.example.com
|
||||
# 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
|
||||
CMS_STORAGE_REGION=ap-southeast-1
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
name: Test and Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['develop']
|
||||
pull_request:
|
||||
branches: ['develop']
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.14.0
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install dependencies
|
||||
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
|
||||
run: npx nx run-many --target=build --all
|
||||
+11
@@ -51,3 +51,14 @@ storybook-static
|
||||
.env.prod
|
||||
.env.develop
|
||||
.env.staging
|
||||
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
|
||||
# Next.js
|
||||
.next
|
||||
out
|
||||
|
||||
Vendored
+2
-1
@@ -2,6 +2,7 @@
|
||||
"recommendations": [
|
||||
"nrwl.angular-console",
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint"
|
||||
"dbaeumer.vscode-eslint",
|
||||
"ms-playwright.playwright"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
usePostLogin,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const useLogin = () => {
|
||||
const postLogin = usePostLogin();
|
||||
@@ -13,14 +13,11 @@ export const useLogin = () => {
|
||||
resolver: zodResolver(authLoginSchema),
|
||||
mode: 'all',
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
postLogin.mutate(data, {
|
||||
onSuccess: () => {
|
||||
console.log('Success Login');
|
||||
navigate('/dashboard');
|
||||
},
|
||||
onSuccess: () => toast.success("Login sukses"),
|
||||
onError: (error) => toast.error(error.message),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { useConfirmItem, useItem } from '../_hook/use-item';
|
||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||
|
||||
interface IModalAddItem {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleAddItem: () => void;
|
||||
handleAddItem?: () => Promise<boolean>;
|
||||
currentStep?: number;
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
@@ -46,91 +48,114 @@ interface IStepOneProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const StepOne = ({ nextStep }: IStepOneProps) => (
|
||||
<>
|
||||
<Modal.Header>
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Tambah Item Gacha
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Lengkapi detail di bawah ini untuk menambahkan item gacha
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-4">
|
||||
<InputField
|
||||
label="Nama Hadiah"
|
||||
type="text"
|
||||
placeholder="Masukkan Nama Hadiah"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<InputField
|
||||
label="Chance Rate"
|
||||
type="text"
|
||||
placeholder="Masukkan Chance Rate"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<InputField
|
||||
label="Foto Barang"
|
||||
type="file"
|
||||
placeholder=".jpg, .jpeg, atau .png"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
const { form, onSubmit } = useItem(nextStep);
|
||||
|
||||
<Button variant="primary" size="lg" className="w-full" onClick={nextStep}>
|
||||
Tambahkan Item
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Modal.Header>
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Tambah Item Gacha
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Lengkapi detail di bawah ini untuk menambahkan item gacha
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-4">
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Nama Hadiah"
|
||||
name="itemName"
|
||||
type="text"
|
||||
placeholder="Masukkan Nama Hadiah"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Quantity"
|
||||
name="quantity"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder="Masukkan Kuantitas Item"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Foto Barang"
|
||||
type="file"
|
||||
name="foto"
|
||||
placeholder=".jpg, .jpeg, atau .png"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={nextStep}
|
||||
>
|
||||
Tambahkan Item
|
||||
</Button>
|
||||
</form>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStepTwoProps {
|
||||
onClose: () => void;
|
||||
handleAddItem?: () => void;
|
||||
handleAddItem?: () => Promise<boolean>;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => (
|
||||
<>
|
||||
<Modal.Header className="mb-0 text-center items-center">
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Tambah Item
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Apakah kamu yakin ingin
|
||||
<br /> menambahkan item ini?
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex gap-4">
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
onClose();
|
||||
resetStep();
|
||||
}}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
handleAddItem && handleAddItem();
|
||||
onClose();
|
||||
resetStep();
|
||||
}}
|
||||
>
|
||||
Tambahkan
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => {
|
||||
const { onConfirm, onCancel } = useConfirmItem(
|
||||
onClose,
|
||||
resetStep,
|
||||
handleAddItem,
|
||||
{
|
||||
success: 'Item ditambahkan ke gacha item',
|
||||
error: 'Item gagal ditambahkan ke gacha item',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal.Header className="mb-0 text-center items-center">
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Tambah Item
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Apakah kamu yakin ingin
|
||||
<br /> menambahkan item ini?
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex gap-4">
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Tambahkan
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalAddItem;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { useConfirmItem, useItem } from '../_hook/use-item';
|
||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||
|
||||
interface IModalEditItem {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleEditItem?: () => void;
|
||||
handleEditItem?: () => Promise<boolean>;
|
||||
currentStep?: number;
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
@@ -46,91 +48,117 @@ interface IStepOneProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const StepOne = ({ nextStep }: IStepOneProps) => (
|
||||
<>
|
||||
<Modal.Header>
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Edit Item Gacha
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Silakan mengubah detail dari item yang diperlukan
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-4">
|
||||
<InputField
|
||||
label="Nama Hadiah"
|
||||
type="text"
|
||||
placeholder="Masukkan Nama Hadiah"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<InputField
|
||||
label="Chance Rate"
|
||||
type="text"
|
||||
placeholder="Masukkan Chance Rate"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<InputField
|
||||
label="Foto Barang"
|
||||
type="file"
|
||||
placeholder=".jpg, .jpeg, atau .png"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
const initialValues = {
|
||||
itemName: 'Hoodie IMPHNEN Official 2025',
|
||||
quantity: 10,
|
||||
};
|
||||
|
||||
<Button variant="primary" size="lg" className="w-full" onClick={nextStep}>
|
||||
Perbarui Item
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
const { form, onSubmit } = useItem(nextStep, initialValues);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal.Header>
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Edit Item Gacha
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Silakan mengubah detail dari item yang diperlukan
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex flex-col gap-8">
|
||||
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-4">
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Nama Hadiah"
|
||||
type="text"
|
||||
name="itemName"
|
||||
placeholder="Masukkan Nama Hadiah"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Quantity"
|
||||
type="number"
|
||||
name="quantity"
|
||||
placeholder="Masukkan Kuantitas Item"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Foto Barang"
|
||||
type="file"
|
||||
name="foto"
|
||||
placeholder=".jpg, .jpeg, atau .png"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={nextStep}
|
||||
>
|
||||
Perbarui Item
|
||||
</Button>
|
||||
</form>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStepTwoProps {
|
||||
onClose: () => void;
|
||||
handleEditItem?: () => void;
|
||||
handleEditItem?: () => Promise<boolean>;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const StepTwo = ({ onClose, handleEditItem, resetStep }: IStepTwoProps) => (
|
||||
<>
|
||||
<Modal.Header className="mb-0 text-center items-center">
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Update Item
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Apakah kamu yakin dengan
|
||||
<br /> perubahan yang dilakukan?
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex gap-4">
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
onClose();
|
||||
resetStep();
|
||||
}}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
handleEditItem && handleEditItem();
|
||||
onClose();
|
||||
resetStep();
|
||||
}}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
const StepTwo = ({ onClose, handleEditItem, resetStep }: IStepTwoProps) => {
|
||||
const { onConfirm, onCancel } = useConfirmItem(
|
||||
onClose,
|
||||
resetStep,
|
||||
handleEditItem,
|
||||
{
|
||||
success: 'Perubahan item berhasil dilakukan',
|
||||
error: 'Perubahan item gagal dilakukan',
|
||||
}
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Modal.Header className="mb-0 text-center items-center">
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Update Item
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Apakah kamu yakin dengan
|
||||
<br /> perubahan yang dilakukan?
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex gap-4">
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalEditItem;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import {
|
||||
gachaItemSchema,
|
||||
TGachaItem
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const useItem = (
|
||||
nextStep: () => void,
|
||||
initialValues?: TGachaItem
|
||||
) => {
|
||||
const form = useForm<TGachaItem>({
|
||||
resolver: zodResolver(gachaItemSchema),
|
||||
mode: 'all',
|
||||
defaultValues: initialValues,
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
console.log('Form data:', data);
|
||||
nextStep();
|
||||
});
|
||||
|
||||
return {
|
||||
form,
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
export const useConfirmItem = (
|
||||
onClose: () => void,
|
||||
resetStep: () => void,
|
||||
actionFunction?: () => Promise<boolean>,
|
||||
messages?: {
|
||||
success?: string;
|
||||
error?: string;
|
||||
}
|
||||
) => {
|
||||
const onConfirm = async () => {
|
||||
try {
|
||||
// const result = await actionFunction?.();
|
||||
// result ? toast.success(messages?.success) : toast.error(messages?.error);
|
||||
toast.success(messages?.success);
|
||||
onClose();
|
||||
resetStep();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error(messages?.error);
|
||||
}
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
onClose();
|
||||
resetStep();
|
||||
};
|
||||
|
||||
return {
|
||||
onConfirm,
|
||||
onCancel,
|
||||
};
|
||||
};
|
||||
@@ -31,20 +31,17 @@ export const Components: FC = (): ReactElement => {
|
||||
return (
|
||||
<Fragment>
|
||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
||||
{/* Dashboard Header */}
|
||||
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
||||
<h1 className="text-p2 font-semibold">Dashboard</h1>
|
||||
</header>
|
||||
|
||||
<div className="flex justify-between gap-[40px] p-8 bg-white rounded-md">
|
||||
<div className="w-full flex flex-col gap-[40px]">
|
||||
{/* Summary Section */}
|
||||
<section>
|
||||
<h2 className="text-p2 font-medium text-primary-500 mb-8">
|
||||
Summary
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Participants */}
|
||||
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
||||
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
||||
<UsergroupAddOutlined className="text-[20px]" />
|
||||
@@ -55,7 +52,6 @@ export const Components: FC = (): ReactElement => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Roll and Reroll */}
|
||||
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
||||
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
||||
<ReloadOutlined className="text-[20px]" />
|
||||
@@ -68,7 +64,6 @@ export const Components: FC = (): ReactElement => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Redeem */}
|
||||
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
||||
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
||||
<UserSwitchOutlined className="text-[20px]" />
|
||||
@@ -79,7 +74,6 @@ export const Components: FC = (): ReactElement => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Inactive Users */}
|
||||
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
||||
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
||||
<UsergroupDeleteOutlined className="text-[20px]" />
|
||||
@@ -94,7 +88,6 @@ export const Components: FC = (): ReactElement => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Gacha Items Section */}
|
||||
<section className="flex flex-col gap-8">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-p2 font-medium text-primary-500">
|
||||
@@ -111,21 +104,20 @@ export const Components: FC = (): ReactElement => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Gacha Items List */}
|
||||
<div className="flex flex-col gap-4 max-h-140 overflow-auto">
|
||||
{[1, 2, 3, 4, 5, 6].map((item) => (
|
||||
<div
|
||||
key={item}
|
||||
className="bg-white max-h-[80px] overflow-clip rounded-lg shadow-sm flex justify-between border border-neutral-100"
|
||||
className="bg-white overflow-clip rounded-lg shadow-sm flex justify-between border border-neutral-100"
|
||||
>
|
||||
<div className="flex flex-col py-4 px-6 gap-4">
|
||||
<div className="flex flex-col py-4 px-6 gap-[8px]">
|
||||
<div>
|
||||
<h3 className="text-p3 text-primary-500 font-medium">
|
||||
Lanyard IMPHNEN
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 text-label2 text-gray-500 mt-1">
|
||||
<div className="flex items-center justify-start gap-10 text-label2 text-gray-500 mt-1">
|
||||
<span>Prize {item}</span>
|
||||
<span>Chance Rate: (0.1%)</span>
|
||||
<span>Quantity: 10</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-start gap-2">
|
||||
@@ -148,12 +140,7 @@ export const Components: FC = (): ReactElement => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lebih baik gunakan gambar yang sudah di-clip dengan size height: 78px daripada hard-code object-position dan margin */}
|
||||
<img
|
||||
src="gacha-clip.webp"
|
||||
alt="Lanyard IMPHNEN"
|
||||
className="h-full"
|
||||
/>
|
||||
<img src="gacha-clip.webp" alt="Lanyard IMPHNEN" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -174,9 +161,6 @@ export const Components: FC = (): ReactElement => {
|
||||
currentStep={currentStep}
|
||||
isOpen={showModalAddItem}
|
||||
onClose={() => setShowModalAddItem(false)}
|
||||
handleAddItem={() => {
|
||||
console.log('Item added');
|
||||
}}
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
@@ -187,9 +171,6 @@ export const Components: FC = (): ReactElement => {
|
||||
currentStep={currentStep}
|
||||
isOpen={showModalEditItem}
|
||||
onClose={() => setShowModalEditItem(false)}
|
||||
handleEditItem={() => {
|
||||
console.log('Item edited');
|
||||
}}
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
@@ -199,9 +180,6 @@ export const Components: FC = (): ReactElement => {
|
||||
<ModalDeleteItem
|
||||
isOpen={showModalDeleteItem}
|
||||
onClose={() => setShowModalDeleteItem(false)}
|
||||
handleDeleteItem={() => {
|
||||
console.log('Item deleted');
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { useItem, useConfirmItem } from '../_hook/use-item';
|
||||
|
||||
interface IModalAddItem {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleAddItem?: () => Promise<boolean>;
|
||||
currentStep?: number;
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const ModalAddItem = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
currentStep,
|
||||
nextStep,
|
||||
resetStep,
|
||||
handleAddItem,
|
||||
}: IModalAddItem) => {
|
||||
return (
|
||||
<Modal
|
||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
||||
isOpen={isOpen}
|
||||
onClose={() => {
|
||||
onClose();
|
||||
resetStep();
|
||||
}}
|
||||
disableEscapeKeyDown={true}
|
||||
>
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
||||
{currentStep === 2 && (
|
||||
<StepTwo
|
||||
onClose={onClose}
|
||||
handleAddItem={handleAddItem}
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStepOneProps {
|
||||
nextStep: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
const { form, onSubmit } = useItem(nextStep);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal.Header>
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Tambah Item Roll Gacha
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Lengkapi detal di bawah ini, untuk menambahkan item gacha
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-4">
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Pilih Item"
|
||||
name="itemName"
|
||||
type="text"
|
||||
placeholder="Pilih item yang dimasukkan ke roll"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Quantity"
|
||||
name="quantity"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder="Masukkan Kuantitas Item"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Chance Rate"
|
||||
name="chanceRate"
|
||||
type="number"
|
||||
value={0.1}
|
||||
min={0.1}
|
||||
step={0.1}
|
||||
max={1}
|
||||
placeholder="Masukkan Chance Rate (0,1 - 1)"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button variant="primary" size="lg" className="w-full" type="submit">
|
||||
Tambahkan Item
|
||||
</Button>
|
||||
</form>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStepTwoProps {
|
||||
onClose: () => void;
|
||||
handleAddItem?: () => Promise<boolean>;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => {
|
||||
const { onConfirm, onCancel } = useConfirmItem(
|
||||
onClose,
|
||||
resetStep,
|
||||
handleAddItem,
|
||||
{
|
||||
success: 'Item ditambahkan ke roll gacha',
|
||||
error: 'Item gagal ditambahkan ke roll gacha',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal.Header className="mb-0 text-center items-center">
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Tambah ke Roll Gacha
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Apakah kamu yakin ingin
|
||||
<br /> menambahkan item ini ke roll gacha?
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex gap-4">
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Tambahkan
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalAddItem;
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
|
||||
interface IModalDeleteItem {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleDeleteItem?: () => void;
|
||||
}
|
||||
|
||||
const ModalDeleteItem = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
handleDeleteItem,
|
||||
}: IModalDeleteItem) => {
|
||||
return (
|
||||
<Modal
|
||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
closeButtonClassName="hidden"
|
||||
>
|
||||
<Modal.Header className="gap-8">
|
||||
<img
|
||||
src="/chibi-delete.webp"
|
||||
alt="Delete item?"
|
||||
width={148}
|
||||
className="self-center"
|
||||
/>
|
||||
<div className="text-center">
|
||||
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
|
||||
Delete Item
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Apakah kamu yakin untuk menghapus item ini?
|
||||
</p>
|
||||
</div>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex gap-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onClose}
|
||||
>
|
||||
Batal Hapus
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
handleDeleteItem && handleDeleteItem();
|
||||
}}
|
||||
>
|
||||
Hapus Item
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalDeleteItem;
|
||||
@@ -0,0 +1,168 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { useConfirmItem, useItem } from '../_hook/use-item';
|
||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||
|
||||
interface IModalUpdateItem {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleUpdateItem?: () => Promise<boolean>;
|
||||
currentStep?: number;
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const ModalUpdateItem = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
currentStep,
|
||||
nextStep,
|
||||
resetStep,
|
||||
handleUpdateItem,
|
||||
}: IModalUpdateItem) => {
|
||||
return (
|
||||
<Modal
|
||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
||||
isOpen={isOpen}
|
||||
onClose={() => {
|
||||
onClose();
|
||||
resetStep();
|
||||
}}
|
||||
disableEscapeKeyDown={true}
|
||||
>
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
||||
{currentStep === 2 && (
|
||||
<StepTwo
|
||||
onClose={onClose}
|
||||
handleUpdateItem={handleUpdateItem}
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStepOneProps {
|
||||
nextStep: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
const initialValues = {
|
||||
itemName: 'Hoodie IMPHNEN Official 2025',
|
||||
quantity: 10,
|
||||
chanceRate: 0.1,
|
||||
};
|
||||
|
||||
const { form, onSubmit } = useItem(nextStep, initialValues);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal.Header>
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Update Item Roll Gacha
|
||||
</h2>
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-4">
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Pilih Item"
|
||||
name="itemName"
|
||||
type="text"
|
||||
placeholder="Pilih item yang dimasukkan ke roll"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Quantity"
|
||||
name="quantity"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder="Masukkan Kuantitas Item"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Chance Rate"
|
||||
name="chanceRate"
|
||||
type="number"
|
||||
value={0.1}
|
||||
min={0.1}
|
||||
step={0.1}
|
||||
max={1}
|
||||
placeholder="Masukkan Chance Rate (0,1 - 1)"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={nextStep}
|
||||
>
|
||||
Perbarui Item
|
||||
</Button>
|
||||
</form>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStepTwoProps {
|
||||
onClose: () => void;
|
||||
handleUpdateItem?: () => Promise<boolean>;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const StepTwo = ({ onClose, handleUpdateItem, resetStep }: IStepTwoProps) => {
|
||||
const { onConfirm, onCancel } = useConfirmItem(
|
||||
onClose,
|
||||
resetStep,
|
||||
handleUpdateItem,
|
||||
{
|
||||
success: 'Perubahan item roll berhasil dilakukan',
|
||||
error: 'Perubahan item roll gagal dilakukan',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal.Header className="mb-0 text-center items-center">
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Update Item
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Apakah kamu yakin dengan
|
||||
<br /> perubahan yang dilakukan?
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex gap-4">
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalUpdateItem;
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import {
|
||||
gachaRollItemSchema,
|
||||
TGachaRollItem
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const useItem = (
|
||||
nextStep: () => void,
|
||||
initialValues?: TGachaRollItem
|
||||
) => {
|
||||
const form = useForm<TGachaRollItem>({
|
||||
resolver: zodResolver(gachaRollItemSchema),
|
||||
mode: 'all',
|
||||
defaultValues: initialValues,
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
console.log('Form data:', data);
|
||||
nextStep();
|
||||
});
|
||||
|
||||
return {
|
||||
form,
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
export const useConfirmItem = (
|
||||
onClose: () => void,
|
||||
resetStep: () => void,
|
||||
actionFunction?: () => Promise<boolean>,
|
||||
messages?: {
|
||||
success?: string;
|
||||
error?: string;
|
||||
}
|
||||
) => {
|
||||
const onConfirm = async () => {
|
||||
try {
|
||||
// const result = await actionFunction?.();
|
||||
// result ? toast.success(messages?.success) : toast.error(messages?.error);
|
||||
toast.success(messages?.success);
|
||||
onClose();
|
||||
resetStep();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error(messages?.error);
|
||||
}
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
onClose();
|
||||
resetStep();
|
||||
};
|
||||
|
||||
return {
|
||||
onConfirm,
|
||||
onCancel,
|
||||
};
|
||||
};
|
||||
@@ -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,210 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
||||
import {
|
||||
SearchOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { DataTable } from '@imphnen-frontend-service/ui/organisms';
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
PaginationState,
|
||||
useReactTable,
|
||||
RowSelectionState,
|
||||
} from '@tanstack/react-table';
|
||||
import ModalAddItem from './_components/modal-add-item';
|
||||
import ModalUpdateItem from './_components/modal-update-item';
|
||||
import ModalDeleteItem from './_components/modal-delete-item';
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||
|
||||
interface GachaItem {
|
||||
id: number;
|
||||
name: string;
|
||||
chanceRate: number;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
const mockData: GachaItem[] = Array.from({ length: 90 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: 'Hoodie IMPHNEN Official 2025',
|
||||
chanceRate: 0.1,
|
||||
quantity: 10,
|
||||
}));
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
||||
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false);
|
||||
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
|
||||
|
||||
const {
|
||||
step: currentStep,
|
||||
nextStep,
|
||||
prevStep,
|
||||
resetStep,
|
||||
} = useQueryState('step', {
|
||||
defaultValue: 1,
|
||||
maxValue: 2,
|
||||
minValue: 1,
|
||||
});
|
||||
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 9,
|
||||
});
|
||||
|
||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
||||
|
||||
const columns: ColumnDef<GachaItem>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded"
|
||||
checked={table.getIsAllRowsSelected()}
|
||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded"
|
||||
checked={row.getIsSelected()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'No',
|
||||
accessorKey: 'id',
|
||||
},
|
||||
{
|
||||
header: 'Nama Item',
|
||||
accessorKey: 'name',
|
||||
},
|
||||
{
|
||||
header: 'Chance Rate',
|
||||
accessorKey: 'chanceRate',
|
||||
},
|
||||
{
|
||||
header: 'Quantity',
|
||||
accessorKey: 'quantity',
|
||||
},
|
||||
{
|
||||
header: 'Action',
|
||||
cell: () => (
|
||||
<div className="flex gap-[8px]">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowModalUpdateItem(true);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<EditOutlined /> Update
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowModalDeleteItem(true);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<DeleteOutlined /> Delete
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: mockData,
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
enableRowSelection: true,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
||||
manualPagination: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
||||
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
||||
<h1 className="text-p2 font-semibold">Gacha Roll</h1>
|
||||
</header>
|
||||
|
||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
||||
<div className="flex justify-between items-center gap-8 mb-2">
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
placeholder="Cari berdasarkan nama item"
|
||||
className="pl-12 w-full max-h-full"
|
||||
/>
|
||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
||||
<SearchOutlined />
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="md"
|
||||
className="flex gap-3 text-nowrap"
|
||||
onClick={() => {
|
||||
setShowModalAddItem(true);
|
||||
}}
|
||||
>
|
||||
<PlusOutlined />
|
||||
Tambah Item
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataTable data={mockData} columns={columns} table={table} />
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<ModalAddItem
|
||||
currentStep={currentStep}
|
||||
isOpen={showModalAddItem}
|
||||
onClose={() => setShowModalAddItem(false)}
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
<ModalUpdateItem
|
||||
currentStep={currentStep}
|
||||
isOpen={showModalUpdateItem}
|
||||
onClose={() => setShowModalUpdateItem(false)}
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
<ModalDeleteItem
|
||||
isOpen={showModalDeleteItem}
|
||||
onClose={() => setShowModalDeleteItem(false)}
|
||||
handleDeleteItem={() => {
|
||||
console.log('Item deleted');
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Components;
|
||||
@@ -0,0 +1,132 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { useItem, useConfirmItem } from '../_hook/use-item';
|
||||
|
||||
interface IModalAddPermission {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleAddItem?: () => Promise<boolean>;
|
||||
currentStep?: number;
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const ModalAddPermission = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
currentStep,
|
||||
nextStep,
|
||||
resetStep,
|
||||
handleAddItem,
|
||||
}: IModalAddPermission) => {
|
||||
return (
|
||||
<Modal
|
||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-3 text-center"
|
||||
isOpen={isOpen}
|
||||
onClose={() => {
|
||||
onClose();
|
||||
resetStep();
|
||||
}}
|
||||
disableEscapeKeyDown={true}
|
||||
>
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
||||
{currentStep === 2 && (
|
||||
<StepTwo
|
||||
onClose={onClose}
|
||||
handleAddItem={handleAddItem}
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStepOneProps {
|
||||
nextStep: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
const { form, onSubmit } = useItem(nextStep);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal.Header>
|
||||
<h2 className="text-p1 font-semibold text-primary-500">
|
||||
Tambah Permissions
|
||||
</h2>
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Name"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Nama Permission"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
|
||||
<Button variant="primary" size="lg" className="w-full" type="submit">
|
||||
Tambah Permission
|
||||
</Button>
|
||||
</form>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStepTwoProps {
|
||||
onClose: () => void;
|
||||
handleAddItem?: () => Promise<boolean>;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => {
|
||||
const { onConfirm, onCancel } = useConfirmItem(
|
||||
onClose,
|
||||
resetStep,
|
||||
handleAddItem,
|
||||
{
|
||||
success: 'Data permissions berhasil ditambahkan',
|
||||
error: 'Data permissions gagal ditambahkan',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal.Header className="mb-7 text-center items-center">
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Tambah Permissions
|
||||
</h2>
|
||||
<p className="text-p3 text-center text-neutral-400">
|
||||
Apakah kamu yakin ingin
|
||||
<br /> menambahkan permission ini?
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex mb-0 gap-4">
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Tambahkan
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalAddPermission;
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { useConfirmItem } from '../_hook/use-item';
|
||||
|
||||
interface IModalDeletePermission {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleDelete?: () => Promise<boolean>;
|
||||
currentStep?: number;
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const ModalDeletePermission = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
resetStep,
|
||||
handleDelete,
|
||||
}: IModalDeletePermission) => {
|
||||
const { onConfirm } = useConfirmItem(onClose, resetStep, handleDelete, {
|
||||
success: 'Data permissions berhasil dihapus',
|
||||
error: 'Data permissions gagal dihapus',
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
closeButtonClassName="hidden"
|
||||
>
|
||||
<Modal.Header className="gap-8">
|
||||
<img
|
||||
src="/chibi-delete.webp"
|
||||
alt="Delete?"
|
||||
width={148}
|
||||
className="self-center"
|
||||
/>
|
||||
<div className="text-center">
|
||||
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
|
||||
Delete Permissions
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Apakah kamu yakin untuk menghapus permission ini? Menghapus data ini
|
||||
mungkin akan mempengaruhi fungsional sistem
|
||||
</p>
|
||||
</div>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex gap-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onClose}
|
||||
>
|
||||
Batal Hapus
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full bg-danger-500 hover:bg-danger-600"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Hapus Item
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalDeletePermission;
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { useConfirmItem } from '../_hook/use-item';
|
||||
|
||||
interface IModalUpdatePermission {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleUpdate?: () => Promise<boolean>;
|
||||
currentStep?: number;
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const ModalUpdatePermission = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
resetStep,
|
||||
handleUpdate,
|
||||
}: IModalUpdatePermission) => {
|
||||
const { onConfirm } = useConfirmItem(onClose, resetStep, handleUpdate, {
|
||||
success: 'Perubahan permissions berhasil dilakukan',
|
||||
error: 'Perubahan permissions gagal dilakukan',
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-3 text-center"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
disableEscapeKeyDown={true}
|
||||
>
|
||||
<Modal.Header>
|
||||
<h2 className="text-p1 font-semibold text-primary-500">
|
||||
Update Permissions
|
||||
</h2>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex flex-col gap-8">
|
||||
<InputField
|
||||
label="Name"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Nama Permission"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
type="submit"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Update Permission
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalUpdatePermission;
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
// import { zodResolver } from '@hookform/resolvers/zod';
|
||||
// import {
|
||||
// gachaRollItemSchema,
|
||||
// TGachaRollItem
|
||||
// } from '@imphnen-frontend-service/service';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const useItem = (
|
||||
nextStep: () => void,
|
||||
initialValues?: any
|
||||
) => {
|
||||
const form = useForm<any>({
|
||||
// resolver: zodResolver(),
|
||||
mode: 'all',
|
||||
defaultValues: initialValues,
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
console.log('Form data:', data);
|
||||
nextStep();
|
||||
});
|
||||
|
||||
return {
|
||||
form,
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
export const useConfirmItem = (
|
||||
onClose: () => void,
|
||||
resetStep: () => void,
|
||||
actionFunction?: () => Promise<boolean>,
|
||||
messages?: {
|
||||
success?: string;
|
||||
error?: string;
|
||||
}
|
||||
) => {
|
||||
const onConfirm = async () => {
|
||||
try {
|
||||
// const result = await actionFunction?.();
|
||||
// result ? toast.success(messages?.success) : toast.error(messages?.error);
|
||||
toast.success(messages?.success);
|
||||
onClose();
|
||||
resetStep();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error(messages?.error);
|
||||
}
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
onClose();
|
||||
resetStep();
|
||||
};
|
||||
|
||||
return {
|
||||
onConfirm,
|
||||
onCancel,
|
||||
};
|
||||
};
|
||||
@@ -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,202 @@
|
||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
||||
import {
|
||||
SearchOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { DataTable } from '@imphnen-frontend-service/ui/organisms';
|
||||
import {
|
||||
ColumnDef,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import ModalAddPermission from './_components/modal-add-permission';
|
||||
import ModalUpdatePermission from './_components/modal-update-permission';
|
||||
import ModalDeletePermission from './_components/modal-delete-permission';
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||
import React from 'react';
|
||||
|
||||
interface Permission {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const mockData: Permission[] = [
|
||||
{ id: 1, name: 'Read' },
|
||||
{ id: 2, name: 'Create' },
|
||||
{ id: 3, name: 'Update' },
|
||||
{ id: 4, name: 'Delete' },
|
||||
];
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
||||
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false);
|
||||
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
|
||||
|
||||
const {
|
||||
step: currentStep,
|
||||
nextStep,
|
||||
prevStep,
|
||||
resetStep,
|
||||
} = useQueryState('step', {
|
||||
defaultValue: 1,
|
||||
maxValue: 2,
|
||||
minValue: 1,
|
||||
});
|
||||
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 9,
|
||||
});
|
||||
|
||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
||||
|
||||
const columns: ColumnDef<Permission>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded"
|
||||
checked={table.getIsAllRowsSelected()}
|
||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded"
|
||||
checked={row.getIsSelected()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'No',
|
||||
accessorKey: 'id',
|
||||
},
|
||||
{
|
||||
header: 'Name',
|
||||
accessorKey: 'name',
|
||||
},
|
||||
{
|
||||
header: 'Action',
|
||||
cell: () => (
|
||||
<div className="flex gap-[8px]">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowModalUpdateItem(true);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<EditOutlined /> Update
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowModalDeleteItem(true);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<DeleteOutlined /> Delete
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: mockData,
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
enableRowSelection: true,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
||||
manualPagination: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
||||
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
||||
<h1 className="text-p2 font-semibold">Permissions</h1>
|
||||
</header>
|
||||
|
||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
||||
<div className="flex justify-between items-center gap-8 mb-2">
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
placeholder="Cari berdasarkan nama permissions"
|
||||
className="pl-12 w-full max-h-full"
|
||||
/>
|
||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
||||
<SearchOutlined />
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="md"
|
||||
className="flex gap-3 text-nowrap"
|
||||
onClick={() => {
|
||||
setShowModalAddItem(true);
|
||||
}}
|
||||
>
|
||||
<PlusOutlined />
|
||||
Tambah Permissionss
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
data={mockData}
|
||||
columns={columns}
|
||||
pageSize={9}
|
||||
table={table}
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<ModalAddPermission
|
||||
currentStep={currentStep}
|
||||
isOpen={showModalAddItem}
|
||||
onClose={() => setShowModalAddItem(false)}
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
<ModalUpdatePermission
|
||||
isOpen={showModalUpdateItem}
|
||||
onClose={() => setShowModalUpdateItem(false)}
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
<ModalDeletePermission
|
||||
isOpen={showModalDeleteItem}
|
||||
onClose={() => setShowModalDeleteItem(false)}
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Components;
|
||||
@@ -0,0 +1,185 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { useItem, useConfirmItem } from '../_hook/use-item';
|
||||
|
||||
interface IModalAddRole {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleAdd?: () => Promise<boolean>;
|
||||
currentStep?: number;
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const ModalAddRole = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
currentStep,
|
||||
nextStep,
|
||||
resetStep,
|
||||
handleAdd,
|
||||
}: IModalAddRole) => {
|
||||
return (
|
||||
<Modal
|
||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-0 text-center"
|
||||
isOpen={isOpen}
|
||||
onClose={() => {
|
||||
onClose();
|
||||
resetStep();
|
||||
}}
|
||||
disableEscapeKeyDown={true}
|
||||
>
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
||||
{currentStep === 2 && (
|
||||
<StepTwo
|
||||
onClose={onClose}
|
||||
handleAdd={handleAdd}
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStepOneProps {
|
||||
nextStep: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
const { form, onSubmit } = useItem(nextStep);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal.Header>
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Tambah Roles
|
||||
</h2>
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-4">
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Nama Role"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Masukkan Nama Role"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 items-start overflow-auto">
|
||||
<span className="text-p3 font-medium text-neutral-800 sticky left-0">
|
||||
Permissions
|
||||
</span>
|
||||
<div className="flex gap-x-6 overflow-x-scroll">
|
||||
{[
|
||||
'Gacha Items',
|
||||
'Gacha Roll',
|
||||
'Roll',
|
||||
'Users',
|
||||
'Gacha Claim',
|
||||
].map((title) => (
|
||||
<div
|
||||
key={title}
|
||||
className="flex flex-col gap-4 select-none text-label2 font-medium text-neutral-900 "
|
||||
>
|
||||
<span className="text-nowrap text-label1">{title}</span>
|
||||
<div className="flex gap-[8px] items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`${title}-all`}
|
||||
className="rounded"
|
||||
/>
|
||||
<label htmlFor={`${title}-all`} className="text-nowrap">
|
||||
Check All
|
||||
</label>
|
||||
</div>
|
||||
<hr className="border-blue-200" />
|
||||
<div className="flex flex-col items-start gap-4 mb-4">
|
||||
<div className="flex gap-[8px] items-center">
|
||||
<input type="checkbox" id={`${title}-read`} />
|
||||
<label htmlFor={`${title}-read`}>Read</label>
|
||||
</div>
|
||||
<div className="flex gap-[8px] items-center">
|
||||
<input type="checkbox" id={`${title}-create`} />
|
||||
<label htmlFor={`${title}-create`}>Create</label>
|
||||
</div>
|
||||
<div className="flex gap-[8px] items-center">
|
||||
<input type="checkbox" id={`${title}-update`} />
|
||||
<label htmlFor={`${title}-update`}>Update</label>
|
||||
</div>
|
||||
<div className="flex gap-[8px] items-center">
|
||||
<input type="checkbox" id={`${title}-delete`} />
|
||||
<label htmlFor={`${title}-delete`}>Delete</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button variant="primary" size="lg" className="w-full" type="submit">
|
||||
Tambah Role
|
||||
</Button>
|
||||
</form>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStepTwoProps {
|
||||
onClose: () => void;
|
||||
handleAdd?: () => Promise<boolean>;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const StepTwo = ({ onClose, handleAdd, resetStep }: IStepTwoProps) => {
|
||||
const { onConfirm, onCancel } = useConfirmItem(
|
||||
onClose,
|
||||
resetStep,
|
||||
handleAdd,
|
||||
{
|
||||
success: 'Data role berhasil ditambahkan',
|
||||
error: 'Data role gagal ditambahkan',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal.Header className="mb-10 text-center items-center">
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Tambah Roles
|
||||
</h2>
|
||||
<p className="text-p3 text-center text-neutral-400">
|
||||
Apakah kamu yakin ingin
|
||||
<br /> menambahkan role ini?
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex mb-0 gap-4">
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Tambahkan
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalAddRole;
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { useConfirmItem } from '../_hook/use-item';
|
||||
|
||||
interface IModalDeletePermission {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleDelete?: () => Promise<boolean>;
|
||||
currentStep?: number;
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const ModalDeletePermission = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
resetStep,
|
||||
handleDelete,
|
||||
}: IModalDeletePermission) => {
|
||||
const { onConfirm } = useConfirmItem(onClose, resetStep, handleDelete, {
|
||||
success: 'Data roles berhasil dihapus',
|
||||
error: 'Data roles gagal dihapus',
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
closeButtonClassName="hidden"
|
||||
>
|
||||
<Modal.Header className="gap-8">
|
||||
<img
|
||||
src="/chibi-delete.webp"
|
||||
alt="Delete?"
|
||||
width={148}
|
||||
className="self-center"
|
||||
/>
|
||||
<div className="text-center">
|
||||
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
|
||||
Delete Roles
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Apakah kamu yakin untuk menghapus role ini? Menghapus data ini
|
||||
mungkin akan mempengaruhi fungsional sistem
|
||||
</p>
|
||||
</div>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex gap-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onClose}
|
||||
>
|
||||
Batal Hapus
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full bg-danger-500 hover:bg-danger-600"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Hapus Role
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalDeletePermission;
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { useConfirmItem } from '../_hook/use-item';
|
||||
|
||||
interface IModalUpdatePermission {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleUpdate?: () => Promise<boolean>;
|
||||
currentStep?: number;
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const ModalUpdatePermission = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
resetStep,
|
||||
handleUpdate,
|
||||
}: IModalUpdatePermission) => {
|
||||
const { onConfirm } = useConfirmItem(onClose, resetStep, handleUpdate, {
|
||||
success: 'Perubahan roles berhasil dilakukan',
|
||||
error: 'Perubahan roles gagal dilakukan',
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-0 text-center"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
disableEscapeKeyDown={true}
|
||||
>
|
||||
<Modal.Header>
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Update Roles
|
||||
</h2>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-4">
|
||||
<InputField
|
||||
label="Nama Role"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Masukkan Nama Role"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 items-start overflow-auto">
|
||||
<span className="text-p3 font-medium text-neutral-800 sticky left-0">
|
||||
Permissions
|
||||
</span>
|
||||
<div className="flex gap-x-6 overflow-x-scroll">
|
||||
{['Gacha Items', 'Gacha Roll', 'Roll', 'Users', 'Gacha Claim'].map(
|
||||
(title) => (
|
||||
<div
|
||||
key={title}
|
||||
className="flex flex-col gap-4 select-none text-label2 font-medium text-neutral-900 "
|
||||
>
|
||||
<span className="text-nowrap text-label1">{title}</span>
|
||||
<div className="flex gap-[8px] items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`${title}-all`}
|
||||
className="rounded"
|
||||
/>
|
||||
<label htmlFor={`${title}-all`} className="text-nowrap">
|
||||
Check All
|
||||
</label>
|
||||
</div>
|
||||
<hr className="border-blue-200" />
|
||||
<div className="flex flex-col items-start gap-4 mb-4">
|
||||
<div className="flex gap-[8px] items-center">
|
||||
<input type="checkbox" id={`${title}-read`} />
|
||||
<label htmlFor={`${title}-read`}>Read</label>
|
||||
</div>
|
||||
<div className="flex gap-[8px] items-center">
|
||||
<input type="checkbox" id={`${title}-create`} />
|
||||
<label htmlFor={`${title}-create`}>Create</label>
|
||||
</div>
|
||||
<div className="flex gap-[8px] items-center">
|
||||
<input type="checkbox" id={`${title}-update`} />
|
||||
<label htmlFor={`${title}-update`}>Update</label>
|
||||
</div>
|
||||
<div className="flex gap-[8px] items-center">
|
||||
<input type="checkbox" id={`${title}-delete`} />
|
||||
<label htmlFor={`${title}-delete`}>Delete</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
type="submit"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Update Role
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalUpdatePermission;
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
// import { zodResolver } from '@hookform/resolvers/zod';
|
||||
// import {
|
||||
// gachaRollItemSchema,
|
||||
// TGachaRollItem
|
||||
// } from '@imphnen-frontend-service/service';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const useItem = (
|
||||
nextStep: () => void,
|
||||
initialValues?: any
|
||||
) => {
|
||||
const form = useForm<any>({
|
||||
// resolver: zodResolver(),
|
||||
mode: 'all',
|
||||
defaultValues: initialValues,
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
console.log('Form data:', data);
|
||||
nextStep();
|
||||
});
|
||||
|
||||
return {
|
||||
form,
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
export const useConfirmItem = (
|
||||
onClose: () => void,
|
||||
resetStep: () => void,
|
||||
actionFunction?: () => Promise<boolean>,
|
||||
messages?: {
|
||||
success?: string;
|
||||
error?: string;
|
||||
}
|
||||
) => {
|
||||
const onConfirm = async () => {
|
||||
try {
|
||||
// const result = await actionFunction?.();
|
||||
// result ? toast.success(messages?.success) : toast.error(messages?.error);
|
||||
toast.success(messages?.success);
|
||||
onClose();
|
||||
resetStep();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error(messages?.error);
|
||||
}
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
onClose();
|
||||
resetStep();
|
||||
};
|
||||
|
||||
return {
|
||||
onConfirm,
|
||||
onCancel,
|
||||
};
|
||||
};
|
||||
@@ -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,203 @@
|
||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
||||
import {
|
||||
SearchOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { DataTable } from '@imphnen-frontend-service/ui/organisms';
|
||||
import {
|
||||
ColumnDef,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import ModalAddRole from './_components/modal-add-role';
|
||||
import ModalUpdateRole from './_components/modal-update-role';
|
||||
import ModalDeleteRole from './_components/modal-delete-role';
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||
import React from 'react';
|
||||
|
||||
interface Role {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const mockData: Role[] = [
|
||||
{ id: 1, name: 'Admin' },
|
||||
{ id: 2, name: 'Admin Pembayaran' },
|
||||
{ id: 3, name: 'Staff' },
|
||||
{ id: 4, name: 'Staff Aktivasi User' },
|
||||
{ id: 5, name: 'User' },
|
||||
];
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
||||
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false);
|
||||
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
|
||||
|
||||
const {
|
||||
step: currentStep,
|
||||
nextStep,
|
||||
prevStep,
|
||||
resetStep,
|
||||
} = useQueryState('step', {
|
||||
defaultValue: 1,
|
||||
maxValue: 2,
|
||||
minValue: 1,
|
||||
});
|
||||
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 9,
|
||||
});
|
||||
|
||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
||||
|
||||
const columns: ColumnDef<Role>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded"
|
||||
checked={table.getIsAllRowsSelected()}
|
||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded"
|
||||
checked={row.getIsSelected()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'ID',
|
||||
accessorKey: 'id',
|
||||
},
|
||||
{
|
||||
header: 'Roles Name',
|
||||
accessorKey: 'name',
|
||||
},
|
||||
{
|
||||
header: 'Action',
|
||||
cell: () => (
|
||||
<div className="flex gap-[8px]">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowModalUpdateItem(true);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<EditOutlined /> Update
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowModalDeleteItem(true);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<DeleteOutlined /> Delete
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: mockData,
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
enableRowSelection: true,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
||||
manualPagination: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
||||
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
||||
<h1 className="text-p2 font-semibold">Roles</h1>
|
||||
</header>
|
||||
|
||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
||||
<div className="flex justify-between items-center gap-8 mb-2">
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
placeholder="Cari berdasarkan nama roles"
|
||||
className="pl-12 w-full max-h-full"
|
||||
/>
|
||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
||||
<SearchOutlined />
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="md"
|
||||
className="flex gap-3 text-nowrap"
|
||||
onClick={() => {
|
||||
setShowModalAddItem(true);
|
||||
}}
|
||||
>
|
||||
<PlusOutlined />
|
||||
Tambah Role
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
data={mockData}
|
||||
columns={columns}
|
||||
pageSize={9}
|
||||
table={table}
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<ModalAddRole
|
||||
currentStep={currentStep}
|
||||
isOpen={showModalAddItem}
|
||||
onClose={() => setShowModalAddItem(false)}
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
<ModalUpdateRole
|
||||
isOpen={showModalUpdateItem}
|
||||
onClose={() => setShowModalUpdateItem(false)}
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
<ModalDeleteRole
|
||||
isOpen={showModalDeleteItem}
|
||||
onClose={() => setShowModalDeleteItem(false)}
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Components;
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
convertPagesToRoute,
|
||||
QueryProvider,
|
||||
} from '@imphnen-frontend-service/utils';
|
||||
import { Toaster } from 'sonner';
|
||||
import './index.css';
|
||||
|
||||
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(
|
||||
<StrictMode>
|
||||
<QueryProvider>
|
||||
<Toaster position="top-right" />
|
||||
<RouterProvider router={router} />
|
||||
</QueryProvider>
|
||||
</StrictMode>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
authLoginSchema,
|
||||
TLoginRequest,
|
||||
usePostLogin,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const useLogin = () => {
|
||||
const postLogin = usePostLogin();
|
||||
const form = useForm<TLoginRequest>({
|
||||
resolver: zodResolver(authLoginSchema),
|
||||
mode: 'all',
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
postLogin.mutate(data, {
|
||||
onSuccess: () => toast.success("Login sukses"),
|
||||
onError: (error) => toast.error(error.message),
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
form,
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
@@ -1,46 +1,50 @@
|
||||
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 { useSearchParams } from 'react-router-dom';
|
||||
import { InputField } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { useLogin } from '../../_hooks/use-login';
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons';
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const error = searchParams.get('error'); // Ambil nilai ?error=
|
||||
const { form, onSubmit } = useLogin();
|
||||
|
||||
return (
|
||||
<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 />
|
||||
<div className="border-2 border-primary-500/50 w-[596px] rounded-lg py-[70px] px-[96px] flex justify-center">
|
||||
<div className="w-[404px]">
|
||||
<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="md:w-[404px]">
|
||||
<h2 className="text-4xl font-semibold text-primary-500 text-center mb-2">
|
||||
Hallo Minna-san
|
||||
</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
|
||||
</h5>
|
||||
<InputField
|
||||
label="Email"
|
||||
error={error ? error : undefined}
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Masukkan email-mu, Senpai~! ✨ (Pastikan tidak typo, ya~ 😆)"
|
||||
/>
|
||||
<InputField
|
||||
label="password"
|
||||
error={error ? error : undefined}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
type="password"
|
||||
placeholder="Masukkan password rahasiamu!"
|
||||
/>
|
||||
<div className="flex justify-end my-5">
|
||||
<a href="/auth/forgot" className="text-primary-500 font-medium">
|
||||
Lupa Password ?
|
||||
</a>
|
||||
</div>
|
||||
<Button className="w-full">Enter Isekai</Button>
|
||||
<form onSubmit={onSubmit} className="flex flex-col gap-2">
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Email"
|
||||
size="lg"
|
||||
className="w-full mb-2"
|
||||
placeholder="Masukkan email-mu, Senpai~! ✨ (Pastikan tidak typo, ya~ 😆)"
|
||||
name={'email'}
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Password"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
type="password"
|
||||
placeholder="Masukkan password rahasiamu!"
|
||||
name={'password'}
|
||||
/>
|
||||
<div className="flex justify-end my-5">
|
||||
<a href="/auth/forgot" className="text-primary-500 font-medium">
|
||||
Lupa Password ?
|
||||
</a>
|
||||
</div>
|
||||
<Button className="w-full" type='submit'>Enter Isekai</Button>
|
||||
</form>
|
||||
|
||||
<div className="flex my-3 gap-3 justify-center">
|
||||
<h5>Belum Punya akun ?</h5>
|
||||
<a href="/auth/register" className="text-primary-500 font-medium">
|
||||
@@ -63,6 +67,13 @@ export const Components: FC = (): ReactElement => {
|
||||
width={24}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
className='xl:hidden w-full gap-3'
|
||||
variant='secondary'
|
||||
>
|
||||
<ArrowLeftOutlined />
|
||||
Kembali Ke Homepage
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,101 @@
|
||||
import { FC, ReactElement } from 'react';
|
||||
import { RegisterResetBanner } from "@imphnen-frontend-service/ui/organisms";
|
||||
import { RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { InputField } from '@imphnen-frontend-service/ui/molecules';
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
return (
|
||||
<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="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">
|
||||
<RegisterResetBanner />
|
||||
<div className='border-2 border-primary-500/50 w-[596px] rounded-lg py-[70px] px-[96px] flex justify-center'>
|
||||
<div className='w-[404px]'>
|
||||
</div>
|
||||
<div className="border-2 border-primary-500/50 w-[696px] rounded-lg py-[32px] px-[48px] flex justify-center">
|
||||
<div className="w-[704px]">
|
||||
<h3 className="mt-5 text-3xl font-semibold text-primary-500">
|
||||
Register
|
||||
</h3>
|
||||
<h5 className="text-primary-500 font-medium">
|
||||
Yosha~! Saatnya Bergabung dengan Dimentorin
|
||||
</h5>
|
||||
<div className="grid grid-flow-row-dense my-7 grid-cols-2 gap-2">
|
||||
<InputField
|
||||
label="First Name"
|
||||
size="lg"
|
||||
placeholder="Nama Depan"
|
||||
/>
|
||||
<InputField
|
||||
label="Last Name"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Nama Belakang"
|
||||
/>
|
||||
<InputField
|
||||
label="Email"
|
||||
size="lg"
|
||||
placeholder="Contoh : yourname@mail.com"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h4 className="font-medium">OTP Code</h4>
|
||||
<Input
|
||||
className="w-full"
|
||||
size="lg"
|
||||
placeholder="Kode OTP"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Button size="sm" className="mt-[30px]">
|
||||
Kirim OTP
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<InputField
|
||||
label="Password"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
type="password"
|
||||
placeholder="Buat password sekeren jurus ultimate-mu!"
|
||||
/>
|
||||
<h6 className="text-sm text-gray-500 mt-1">
|
||||
"Senpai~! Pastikan password-mu sekuat pertahanan kastil!"
|
||||
</h6>
|
||||
<h6 className="text-sm text-gray-500 mt-2">
|
||||
Tips membuat password yang OP:
|
||||
</h6>
|
||||
<h6 className="text-sm text-gray-500">
|
||||
- Minimal 8 karakter (semakin panjang, semakin power-up!{' '}
|
||||
<span role="img" aria-label="emoji">
|
||||
⚡
|
||||
</span>
|
||||
)
|
||||
</h6>
|
||||
<h6 className="text-sm text-gray-500">
|
||||
- Campur huruf besar, kecil, angka, dan simbol untuk kombinasi
|
||||
ultimate!
|
||||
<span role="img" aria-label="emoji">
|
||||
🔥
|
||||
</span>
|
||||
</h6>
|
||||
<h6 className="text-sm text-gray-500 mb-2">
|
||||
- Jangan pakai password yang gampang ditebak, nanti ketahuan
|
||||
musuh!
|
||||
<span role="img" aria-label="emoji">
|
||||
🚨
|
||||
</span>
|
||||
</h6>
|
||||
<InputField
|
||||
label="Repeat Password"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
type="password"
|
||||
placeholder="Ulangi password-mu, Senpai~!"
|
||||
/>
|
||||
<Button className="w-full mt-2">Linked Start !!!</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Components;
|
||||
export default Components;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { FC, ReactElement } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
export const AppLayout: FC = (): ReactElement => {
|
||||
return (
|
||||
<main className="bg-primary-50 min-h-screen">
|
||||
<Outlet />
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLayout;
|
||||
@@ -0,0 +1,55 @@
|
||||
import { FC, ReactElement } from 'react';
|
||||
import { RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { ArrowRightOutlined } from '@ant-design/icons';
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
return (
|
||||
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
|
||||
<div className="bg-white min-w-[1220px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6">
|
||||
<RegisterResetBanner />
|
||||
<div className="border-2 border-primary-500/50 w-[696px] rounded-lg py-[32px] px-[48px] flex justify-center bg-primary-50">
|
||||
<div className="w-[704px] flex flex-col justify-center items-center">
|
||||
<h2 className="text-4xl text-primary-500 font-semibold">
|
||||
Register Successfull
|
||||
</h2>
|
||||
<h4 className="mt-3 text-primary-500 font-medium text-xl">
|
||||
Yosha~! Saatnya Bergabung dengan Dimentorin!
|
||||
</h4>
|
||||
<svg
|
||||
width="198"
|
||||
height="198"
|
||||
viewBox="0 0 198 198"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="mt-10"
|
||||
>
|
||||
<circle cx="99" cy="99" r="99" fill="#BCF8B0" />
|
||||
<rect
|
||||
x="31.1152"
|
||||
y="32.9141"
|
||||
width="133.971"
|
||||
height="133.971"
|
||||
rx="66.9857"
|
||||
fill="#35BA43"
|
||||
/>
|
||||
<path
|
||||
d="M127.935 75.8846H122.722C121.991 75.8846 121.297 76.2202 120.85 76.7945L90.0997 115.748L75.355 97.0657C75.1319 96.7825 74.8476 96.5535 74.5234 96.3959C74.1992 96.2384 73.8435 96.1563 73.483 96.1558H68.2697C67.77 96.1558 67.4941 96.7301 67.7999 97.1179L88.2277 122.998C89.1824 124.206 91.0171 124.206 91.9792 122.998L128.405 76.8392C128.711 76.4589 128.435 75.8846 127.935 75.8846Z"
|
||||
fill="#E0FBD8"
|
||||
/>
|
||||
</svg>
|
||||
<h4 className="mt-10 text-primary-500 font-medium text-xl">
|
||||
Kamu akan memasuki isekai dalam 10 dtk
|
||||
</h4>
|
||||
<Button className="gap-3 mt-10" size="lg">
|
||||
Masuk Isekai
|
||||
<ArrowRightOutlined />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Components;
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
convertPagesToRoute,
|
||||
QueryProvider,
|
||||
} from '@imphnen-frontend-service/utils';
|
||||
import { Toaster } from 'sonner';
|
||||
import './index.css';
|
||||
|
||||
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(
|
||||
<StrictMode>
|
||||
<QueryProvider>
|
||||
<Toaster position="top-right" />
|
||||
<RouterProvider router={router} />
|
||||
</QueryProvider>
|
||||
</StrictMode>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRegister } from '../../_hooks/use-register';
|
||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
import { TRegisterRequest } from '@imphnen-frontend-service/service';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
interface IModalFormRegisterProps {
|
||||
isOpen: boolean;
|
||||
@@ -12,7 +13,7 @@ interface IModalFormRegisterProps {
|
||||
}
|
||||
|
||||
const ModalFormRegister = ({ isOpen, onClose }: IModalFormRegisterProps) => {
|
||||
const { form, onSubmit } = useRegister();
|
||||
const { form, onSubmit, isStepOneValid } = useRegister();
|
||||
|
||||
const {
|
||||
step: currentStep,
|
||||
@@ -25,6 +26,12 @@ const ModalFormRegister = ({ isOpen, onClose }: IModalFormRegisterProps) => {
|
||||
minValue: 1,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isStepOneValid && currentStep === 2) {
|
||||
prevStep();
|
||||
}
|
||||
}, [isStepOneValid, currentStep, prevStep]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7"
|
||||
@@ -46,7 +53,11 @@ const ModalFormRegister = ({ isOpen, onClose }: IModalFormRegisterProps) => {
|
||||
<Modal.Content>
|
||||
<form onSubmit={onSubmit} className="space-y-6">
|
||||
{currentStep === 1 && (
|
||||
<StepOne form={form} nextStep={nextStep} onClose={onClose} />
|
||||
<StepOne
|
||||
form={form}
|
||||
nextStep={nextStep}
|
||||
isStepOneValid={isStepOneValid}
|
||||
/>
|
||||
)}
|
||||
{currentStep === 2 && <StepTwo form={form} prevStep={prevStep} />}
|
||||
</form>
|
||||
@@ -58,52 +69,59 @@ const ModalFormRegister = ({ isOpen, onClose }: IModalFormRegisterProps) => {
|
||||
interface IStepOneProps {
|
||||
form: UseFormReturn<TRegisterRequest, any, TRegisterRequest>;
|
||||
nextStep: () => void;
|
||||
onClose: () => void;
|
||||
isStepOneValid: boolean;
|
||||
}
|
||||
|
||||
const StepOne = ({ form, nextStep, onClose }: IStepOneProps) => (
|
||||
<>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
name="fullname"
|
||||
label="Nama Lengkap"
|
||||
placeholder="Masukkan Nama Lengkap"
|
||||
type="text"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Email"
|
||||
placeholder="Masukkan Email Anda"
|
||||
type="email"
|
||||
name="email"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="Masukkan Password"
|
||||
type="password"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
name="confirm_password"
|
||||
label="Ulangi Password"
|
||||
placeholder="Masukkan Ulang Password"
|
||||
type="password"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<Button size="md" className="w-full" onClick={nextStep}>
|
||||
Selanjutnya
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
const StepOne = ({ form, nextStep, isStepOneValid }: IStepOneProps) => {
|
||||
return (
|
||||
<>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
name="fullname"
|
||||
label="Nama Lengkap"
|
||||
placeholder="Masukkan Nama Lengkap"
|
||||
type="text"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Email"
|
||||
placeholder="Masukkan Email Anda"
|
||||
type="email"
|
||||
name="email"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="Masukkan Password"
|
||||
type="password"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
name="confirm_password"
|
||||
label="Ulangi Password"
|
||||
placeholder="Masukkan Ulang Password"
|
||||
type="password"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<Button
|
||||
size="md"
|
||||
className="w-full"
|
||||
onClick={nextStep}
|
||||
disabled={!isStepOneValid}
|
||||
>
|
||||
Selanjutnya
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStepTwoProps {
|
||||
form: UseFormReturn<TRegisterRequest, any, TRegisterRequest>;
|
||||
|
||||
@@ -1,19 +1,48 @@
|
||||
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();
|
||||
const form = useForm<TRegisterRequest>({
|
||||
resolver: zodResolver(authRegisterSchema),
|
||||
mode: 'all',
|
||||
defaultValues: {
|
||||
fullname: '',
|
||||
email: '',
|
||||
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: (data) => toast.success(data.message),
|
||||
@@ -24,5 +53,6 @@ export const useRegister = () => {
|
||||
return {
|
||||
form,
|
||||
onSubmit,
|
||||
isStepOneValid: stepValid,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"jsc": {
|
||||
"target": "es2017",
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"decorators": true,
|
||||
"dynamicImport": true
|
||||
},
|
||||
"transform": {
|
||||
"decoratorMetadata": true,
|
||||
"legacyDecorator": true
|
||||
},
|
||||
"keepClassNames": true,
|
||||
"externalHelpers": true,
|
||||
"loose": true
|
||||
},
|
||||
"module": {
|
||||
"type": "commonjs"
|
||||
},
|
||||
"sourceMaps": true,
|
||||
"exclude": [
|
||||
"jest.config.ts",
|
||||
".*\\.spec.tsx?$",
|
||||
".*\\.test.tsx?$",
|
||||
"./src/jest-setup.ts$",
|
||||
"./**/jest-setup.ts$",
|
||||
".*.js$",
|
||||
".*.d.ts$"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { FlatCompat } from '@eslint/eslintrc';
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import js from '@eslint/js';
|
||||
import { fixupConfigRules } from '@eslint/compat';
|
||||
import nx from '@nx/eslint-plugin';
|
||||
import baseConfig from '../../eslint.config.mjs';
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: dirname(fileURLToPath(import.meta.url)),
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
|
||||
export default [
|
||||
...fixupConfigRules(compat.extends('next')),
|
||||
...fixupConfigRules(compat.extends('next/core-web-vitals')),
|
||||
...baseConfig,
|
||||
...nx.configs['flat/react-typescript'],
|
||||
{
|
||||
ignores: ['.next/**/*'],
|
||||
},
|
||||
];
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
declare module '*.svg' {
|
||||
const content: any;
|
||||
export const ReactComponent: any;
|
||||
export default content;
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -0,0 +1,15 @@
|
||||
const { composePlugins, withNx } = require('@nx/next');
|
||||
const { withPayload } = require('@payloadcms/next/withPayload');
|
||||
|
||||
/** @type {import('@nx/next/plugins/with-nx').WithNxOptions} */
|
||||
const nextConfig = {
|
||||
nx: { svgr: false },
|
||||
sassOptions: {
|
||||
quietDeps: true,
|
||||
logger: {
|
||||
warn: () => {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = composePlugins(withNx, withPayload)(nextConfig);
|
||||
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "landing",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "apps/landing",
|
||||
"projectType": "application",
|
||||
"tags": [],
|
||||
"// targets": "to see all targets run: nx show project landing --web",
|
||||
"targets": {
|
||||
"serve": {
|
||||
"executor": "@nx/next:server",
|
||||
"options": {
|
||||
"buildTarget": "landing:build",
|
||||
"dev": true
|
||||
}
|
||||
},
|
||||
"build": {
|
||||
"executor": "@nx/next:build",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/apps/landing"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 159 KiB |
@@ -0,0 +1,78 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@imphnen-frontend-service/shadcn-ui/atoms';
|
||||
import { CallToActionSection } from 'apps/landing/src/payload-types';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import { useRef } from 'react';
|
||||
|
||||
export function CallToAction(props: CallToActionSection) {
|
||||
const {
|
||||
title,
|
||||
highlightedTitle,
|
||||
subtitle,
|
||||
primaryButtonLabel,
|
||||
primaryButtonLink,
|
||||
secondaryButtonLabel,
|
||||
secondaryButtonLink,
|
||||
} = props;
|
||||
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, amount: 0.2 });
|
||||
|
||||
return (
|
||||
<section className="w-full py-20 md:py-32 relative overflow-hidden">
|
||||
{/* Background Elements */}
|
||||
<div className="absolute inset-0 -z-10">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-background to-primary/20" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(59,130,246,0.2),transparent_70%)]" />
|
||||
</div>
|
||||
|
||||
<div className="container px-4 md:px-6" ref={ref}>
|
||||
<motion.div
|
||||
className="max-w-4xl mx-auto rounded-2xl overflow-hidden border shadow-lg"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<div className="relative p-8 md:p-12 lg:p-16 bg-background">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-blue-400/5" />
|
||||
|
||||
<div className="relative z-10 text-center">
|
||||
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold mb-6">
|
||||
{title}{' '}
|
||||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
||||
{highlightedTitle}
|
||||
</span>
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground mb-8 max-w-2xl mx-auto">
|
||||
{subtitle}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90 font-bold text-black hover:text-white cursor-pointer"
|
||||
onClick={() => window.open(primaryButtonLink, '_blank')}
|
||||
>
|
||||
{primaryButtonLabel}
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="border-primary hover:bg-primary/10"
|
||||
onClick={() => window.open(secondaryButtonLink, '_blank')}
|
||||
>
|
||||
{secondaryButtonLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decorative Elements */}
|
||||
<div className="absolute -top-12 -left-12 w-24 h-24 rounded-full bg-primary/10 blur-2xl" />
|
||||
<div className="absolute -bottom-12 -right-12 w-24 h-24 rounded-full bg-blue-400/10 blur-2xl" />
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
'use client';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import { Button } from '@imphnen-frontend-service/shadcn-ui/atoms';
|
||||
import { CommunitiesSection } from 'apps/landing/src/payload-types';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import { useRef } from 'react';
|
||||
|
||||
export function Community(props: CommunitiesSection) {
|
||||
const { items, stats } = props;
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, amount: 0.2 });
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: { duration: 0.5 },
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
id="komunitas"
|
||||
className="w-full py-20 md:py-32 bg-muted relative overflow-hidden"
|
||||
>
|
||||
<div className="absolute inset-0 -z-10">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(59,130,246,0.1),transparent_70%)]" />
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,rgba(59,130,246,0.01)_1px,transparent_1px),linear-gradient(to_bottom,rgba(59,130,246,0.01)_1px,transparent_1px)] bg-[size:14px_14px]" />
|
||||
</div>
|
||||
<div className="container px-4 md:px-6" ref={ref}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||
>
|
||||
<h2 className="text-3xl font-bold tracking-tighter md:text-4xl/tight lg:text-5xl text-center mb-4">
|
||||
Komunitas{' '}
|
||||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
||||
Kami
|
||||
</span>
|
||||
</h2>
|
||||
<p className="max-w-[800px] mx-auto text-muted-foreground text-center md:text-lg">
|
||||
Bergabunglah dengan ribuan programmer Indonesia yang saling membantu
|
||||
dan berbagi pengalaman.
|
||||
</p>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="grid gap-8 md:grid-cols-3 mt-12"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
>
|
||||
{items?.map((c, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
className="group relative overflow-hidden rounded-xl border bg-background p-6 transition-all hover:shadow-xl"
|
||||
variants={itemVariants}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-blue-400/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
<div className="relative z-10">
|
||||
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900">
|
||||
<Icon
|
||||
icon={c.iconName}
|
||||
className="h-6 w-6 text-blue-600 dark:text-blue-400"
|
||||
/>
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl font-bold">{c.title}</h3>
|
||||
<p className="mb-6 text-muted-foreground">{c.description}</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full group-hover:bg-primary group-hover:text-primary-foreground transition-colors duration-300"
|
||||
onClick={() => window.open(c.buttonLink, '_blank')}
|
||||
>
|
||||
{c.buttonText}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="absolute -bottom-1 -right-1 w-20 h-20 bg-gradient-to-tl from-primary/20 to-transparent rounded-tl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="mt-20 grid grid-cols-2 md:grid-cols-4 gap-8 text-center"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
>
|
||||
{stats?.map((s, i) => (
|
||||
<div key={i} className="space-y-2">
|
||||
<div className="text-4xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
||||
{s.value}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">{s.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
'use client';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import { FeaturesSection } from 'apps/landing/src/payload-types';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import { useRef } from 'react';
|
||||
|
||||
export function Features(props: FeaturesSection) {
|
||||
const { heading, subheading, features } = props;
|
||||
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, amount: 0.2 });
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: { duration: 0.5 },
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
id="fitur"
|
||||
className="w-full py-20 md:py-32 relative overflow-hidden"
|
||||
>
|
||||
<div className="absolute inset-0 -z-10">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-background via-background to-muted/50" />
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] rounded-full bg-gradient-to-tr from-primary/5 to-blue-400/5 blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className="container px-4 md:px-6" ref={ref}>
|
||||
<div className="flex flex-col items-center justify-center space-y-4 text-center mb-16">
|
||||
<motion.div
|
||||
className="space-y-2"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<div className="inline-block rounded-full bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary">
|
||||
Fitur Unggulan
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold tracking-tighter md:text-4xl/tight lg:text-5xl">
|
||||
{heading}
|
||||
</h2>
|
||||
<p className="max-w-[800px] mx-auto text-muted-foreground md:text-lg">
|
||||
{subheading}
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="grid gap-8 md:grid-cols-2 lg:grid-cols-4"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
>
|
||||
{features?.map((feature, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
className="group relative overflow-hidden rounded-xl border bg-background/50 backdrop-blur-sm p-6 transition-all hover:shadow-md hover:shadow-primary/5 hover:border-primary/50"
|
||||
variants={itemVariants}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-blue-400/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 group-hover:bg-primary/20 transition-colors">
|
||||
<Icon
|
||||
icon={feature.iconName}
|
||||
className="h-6 w-6 text-primary"
|
||||
/>
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl font-bold">{feature.title}</h3>
|
||||
<p className="text-muted-foreground">{feature.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-0 left-0 h-1 w-0 bg-gradient-to-r from-primary to-blue-400 group-hover:w-full transition-all duration-300" />
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="w-full border-t bg-background py-12 md:py-16">
|
||||
<div className="container px-4 md:px-6">
|
||||
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
||||
IMPHNEN
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Ingin Menjadi Programmer Handal merupakan komunitas Programmer
|
||||
Indonesia.
|
||||
</p>
|
||||
<div className="flex space-x-4">
|
||||
<Link
|
||||
href="#"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z" />
|
||||
</svg>
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M9 8h-3v4h3v12h5v-12h3.642l.358-4h-4v-1.667c0-.955.192-1.333 1.115-1.333h2.885v-5h-3.808c-3.596 0-5.192 1.583-5.192 4.615v3.385z" />
|
||||
</svg>
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z" />
|
||||
</svg>
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M19.615 3.184c-3.604-.246-11.631-.245-15.23 0-3.897.266-4.356 2.62-4.385 8.816.029 6.185.484 8.549 4.385 8.816 3.6.245 11.626.246 15.23 0 3.897-.266 4.356-2.62 4.385-8.816-.029-6.185-.484-8.549-4.385-8.816zm-10.615 12.816v-8l8 3.993-8 4.007z" />
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-bold">Tautan Cepat</h3>
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<Link
|
||||
href="#fitur"
|
||||
className="text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Fitur
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="#komunitas"
|
||||
className="text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Komunitas
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="#sumber-belajar"
|
||||
className="text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Sumber Belajar
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="#testimoni"
|
||||
className="text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Testimoni
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-bold">Sumber Belajar</h3>
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Video Tutorial
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Artikel
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Tantangan Koding
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Sharing Session
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-bold">Bahasa Pemrograman</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="inline-flex items-center rounded-md bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-600/10 dark:bg-blue-900/30 dark:text-blue-400 dark:ring-blue-400/20">
|
||||
PHP
|
||||
</span>
|
||||
<span className="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-yellow-600/10 dark:bg-yellow-900/30 dark:text-yellow-400 dark:ring-yellow-400/20">
|
||||
JavaScript
|
||||
</span>
|
||||
<span className="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/10 dark:bg-green-900/30 dark:text-green-400 dark:ring-green-400/20">
|
||||
Python
|
||||
</span>
|
||||
<span className="inline-flex items-center rounded-md bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-600/10 dark:bg-blue-900/30 dark:text-blue-400 dark:ring-blue-400/20">
|
||||
C#
|
||||
</span>
|
||||
<span className="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-600/10 dark:bg-red-900/30 dark:text-red-400 dark:ring-red-400/20">
|
||||
Java
|
||||
</span>
|
||||
<span className="inline-flex items-center rounded-md bg-cyan-50 px-2 py-1 text-xs font-medium text-cyan-700 ring-1 ring-inset ring-cyan-600/10 dark:bg-cyan-900/30 dark:text-cyan-400 dark:ring-cyan-400/20">
|
||||
Go
|
||||
</span>
|
||||
<span className="inline-flex items-center rounded-md bg-orange-50 px-2 py-1 text-xs font-medium text-orange-700 ring-1 ring-inset ring-orange-600/10 dark:bg-orange-900/30 dark:text-orange-400 dark:ring-orange-400/20">
|
||||
Rust
|
||||
</span>
|
||||
<span className="inline-flex items-center rounded-md bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-600/10 dark:bg-blue-900/30 dark:text-blue-400 dark:ring-blue-400/20">
|
||||
HTML
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-bold mb-2">Newsletter</h3>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Dapatkan update terbaru dari kami
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email Anda"
|
||||
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
/>
|
||||
<button className="inline-flex items-center justify-center rounded-md bg-primary px-3 py-1 text-sm shadow hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50
|
||||
font-bold text-black hover:text-white cursor-pointer">
|
||||
Daftar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 border-t pt-8 text-center">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
© {new Date().getFullYear()} IMPHNEN - Ingin Menjadi Programmer
|
||||
Handal, Namun Enggan Ngoding. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Button,
|
||||
MenuIcon,
|
||||
XIcon,
|
||||
} from '@imphnen-frontend-service/shadcn-ui/atoms';
|
||||
import { cn } from '@imphnen-frontend-service/utils';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { SimpleThemeToggle } from './simple-theme-toggle';
|
||||
|
||||
export function Header() {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > 10);
|
||||
};
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
'sticky top-0 z-50 w-full transition-all duration-300',
|
||||
isScrolled
|
||||
? 'bg-background/80 backdrop-blur-md border-b shadow-sm'
|
||||
: 'bg-transparent'
|
||||
)}
|
||||
>
|
||||
<div className="container flex h-16 items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative overflow-hidden rounded">
|
||||
<Link href="/">
|
||||
<Image
|
||||
src="/logo.png"
|
||||
alt="IMPHNEN"
|
||||
width={64}
|
||||
height={64}
|
||||
className="object-cover"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden md:flex items-center gap-8">
|
||||
<Link href="#fitur" className="text-sm font-medium relative group">
|
||||
<span className="transition-colors hover:text-primary">Fitur</span>
|
||||
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
|
||||
</Link>
|
||||
<Link
|
||||
href="#komunitas"
|
||||
className="text-sm font-medium relative group"
|
||||
>
|
||||
<span className="transition-colors hover:text-primary">
|
||||
Komunitas
|
||||
</span>
|
||||
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
|
||||
</Link>
|
||||
<Link
|
||||
href="#sumber-belajar"
|
||||
className="text-sm font-medium relative group"
|
||||
>
|
||||
<span className="transition-colors hover:text-primary">
|
||||
Sumber Belajar
|
||||
</span>
|
||||
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
|
||||
</Link>
|
||||
<Link
|
||||
href="#testimoni"
|
||||
className="text-sm font-medium relative group"
|
||||
>
|
||||
<span className="transition-colors hover:text-primary">
|
||||
Testimoni
|
||||
</span>
|
||||
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<SimpleThemeToggle />
|
||||
|
||||
<Button
|
||||
className="hidden md:flex 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('https://discord.com/invite/imphnen', '_blank')
|
||||
}
|
||||
>
|
||||
Gabung Discord
|
||||
</Button>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="md:hidden"
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
>
|
||||
{mobileMenuOpen ? (
|
||||
<XIcon className="h-6 w-6" />
|
||||
) : (
|
||||
<MenuIcon className="h-6 w-6" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="md:hidden border-t bg-background/95 backdrop-blur-md">
|
||||
<nav className="container flex flex-col py-4 text-center">
|
||||
<Link
|
||||
href="#fitur"
|
||||
className="py-3 text-sm font-medium border-b border-border/50"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
Fitur
|
||||
</Link>
|
||||
<Link
|
||||
href="#komunitas"
|
||||
className="py-3 text-sm font-medium border-b border-border/50"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
Komunitas
|
||||
</Link>
|
||||
<Link
|
||||
href="#sumber-belajar"
|
||||
className="py-3 text-sm font-medium border-b border-border/50"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
Sumber Belajar
|
||||
</Link>
|
||||
<Link
|
||||
href="#testimoni"
|
||||
className="py-3 text-sm font-medium"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
Testimoni
|
||||
</Link>
|
||||
<a
|
||||
href="https://discord.com/invite/imphnen"
|
||||
target="_blank"
|
||||
className="mt-4"
|
||||
>
|
||||
<Button className="mt-4 bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90 font-bold text-black hover:text-white cursor-pointer">
|
||||
Gabung Discord
|
||||
</Button>
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Button,
|
||||
CodeIcon,
|
||||
SparklesIcon,
|
||||
UsersIcon,
|
||||
} from '@imphnen-frontend-service/shadcn-ui/atoms';
|
||||
import { formatCMSImageDataToMedia } from 'apps/landing/src/lib/format';
|
||||
import { HeroSection } from 'apps/landing/src/payload-types';
|
||||
import { motion } from 'framer-motion';
|
||||
import Image from 'next/image';
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
|
||||
export function Hero(props: HeroSection) {
|
||||
const {
|
||||
badgeText,
|
||||
buttons,
|
||||
description,
|
||||
highlight,
|
||||
title,
|
||||
stats,
|
||||
heroImage,
|
||||
} = props;
|
||||
|
||||
const [scrollY, setScrollY] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setScrollY(window.scrollY);
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="relative w-full py-20 md:py-32 lg:py-40 overflow-hidden">
|
||||
<div className="absolute inset-0 -z-10 overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-gradient-to-b from-background to-background/50" />
|
||||
<div
|
||||
className="absolute top-1/4 -left-20 w-80 h-80 rounded-full bg-gradient-to-r from-primary/20 to-blue-400/20 blur-3xl"
|
||||
style={{
|
||||
transform: `translate(${scrollY * 0.1}px, ${scrollY * -0.05}px)`,
|
||||
opacity: Math.max(0.2, 1 - scrollY * 0.001),
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute bottom-1/3 -right-20 w-80 h-80 rounded-full bg-gradient-to-r from-blue-400/20 to-primary/20 blur-3xl"
|
||||
style={{
|
||||
transform: `translate(${scrollY * -0.1}px, ${scrollY * 0.05}px)`,
|
||||
opacity: Math.max(0.2, 1 - scrollY * 0.001),
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-[linear-gradient(rgba(59,130,246,0.05)_1px,transparent_1px),linear-gradient(to_right,rgba(59,130,246,0.05)_1px,transparent_1px)] bg-[size:40px_40px]" />
|
||||
</div>
|
||||
|
||||
<div className="container px-4 md:px-6 relative">
|
||||
<div className="grid gap-6 lg:grid-cols-2 lg:gap-12 items-center">
|
||||
<motion.div
|
||||
className="flex flex-col justify-center space-y-8"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<div className="inline-flex items-center rounded-full border px-3 py-1 text-sm w-fit">
|
||||
<SparklesIcon className="mr-1 h-3.5 w-3.5 text-primary" />
|
||||
<span>{badgeText}</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<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}
|
||||
</span>
|
||||
</h1>
|
||||
<p className="max-w-[600px] text-muted-foreground md:text-xl">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<Button
|
||||
size="lg"
|
||||
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}
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
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" />
|
||||
<span className="relative">{buttons.secondaryLabel}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-8">
|
||||
{stats?.map(({ value, label }, i) => (
|
||||
<Fragment key={i}>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="text-2xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
||||
{value}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">{label}</div>
|
||||
</div>
|
||||
{i < stats.length - 1 && (
|
||||
<div className="h-10 border-r border-border mx-4" />
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
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 }}
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="absolute -top-6 -left-6 w-12 h-12 rounded-lg border border-primary/30 bg-background/50 backdrop-blur-sm flex items-center justify-center">
|
||||
<CodeIcon className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div className="absolute -bottom-6 -right-6 w-12 h-12 rounded-lg border border-blue-400/30 bg-background/50 backdrop-blur-sm flex items-center justify-center">
|
||||
<UsersIcon className="h-6 w-6 text-blue-400" />
|
||||
</div>
|
||||
<div className="relative z-10 rounded-2xl overflow-hidden border shadow-2xl">
|
||||
<div className="absolute inset-0 bg-gradient-to-tr from-primary/10 to-blue-400/10" />
|
||||
{(() => {
|
||||
const media = formatCMSImageDataToMedia(heroImage);
|
||||
if (!media) return null;
|
||||
return (
|
||||
<Image
|
||||
src={media.url!}
|
||||
alt={media.alt}
|
||||
width={600}
|
||||
height={500}
|
||||
className="w-full h-auto object-cover"
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
'use client';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import { Button } from '@imphnen-frontend-service/shadcn-ui/atoms';
|
||||
import { LearningResourcesSection } from 'apps/landing/src/payload-types';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import { useRef } from 'react';
|
||||
|
||||
export function LearningResources(props: LearningResourcesSection) {
|
||||
const { resources, featured, title, description } = props;
|
||||
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, amount: 0.2 });
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: { duration: 0.5 },
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
id="sumber-belajar"
|
||||
className="w-full py-20 md:py-32 relative overflow-hidden"
|
||||
>
|
||||
<div className="absolute inset-0 -z-10">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-background via-background to-background" />
|
||||
<div className="absolute top-0 right-0 w-1/2 h-1/2 bg-gradient-to-bl from-primary/5 to-transparent blur-3xl" />
|
||||
<div className="absolute bottom-0 left-0 w-1/2 h-1/2 bg-gradient-to-tr from-blue-400/5 to-transparent blur-3xl" />
|
||||
</div>
|
||||
<div className="container px-4 md:px-6" ref={ref}>
|
||||
<div className="flex flex-col items-center justify-center space-y-4 text-center mb-16">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<h2 className="text-3xl font-bold tracking-tighter md:text-4xl/tight lg:text-5xl">
|
||||
{title}
|
||||
</h2>
|
||||
<p className="max-w-[800px] mx-auto text-muted-foreground md:text-lg">
|
||||
{description}
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
<motion.div
|
||||
className="grid gap-8 md:grid-cols-2 lg:grid-cols-4"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
>
|
||||
{resources.map((r, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
className="group relative overflow-hidden rounded-xl border bg-background p-6 hover:shadow-lg"
|
||||
variants={itemVariants}
|
||||
>
|
||||
<div className="absolute top-0 left-0 h-1 w-full bg-gradient-to-r from-primary/50 to-blue-400/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
<div className="relative z-10">
|
||||
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900">
|
||||
<Icon
|
||||
icon={r.icon}
|
||||
className="h-6 w-6 text-blue-600 dark:text-blue-400"
|
||||
/>
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl font-bold">{r.title}</h3>
|
||||
<p className="mb-6 text-muted-foreground">{r.description}</p>
|
||||
|
||||
<Button
|
||||
variant="link"
|
||||
className="p-0 h-auto font-medium text-primary hover:text-primary/80"
|
||||
onClick={() => window.open(r.buttonLink, '_blank')}
|
||||
>
|
||||
{r.buttonText}
|
||||
<Icon icon="tabler:arrow-right" className="h-6 w-6" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="absolute -bottom-32 -right-32 w-64 h-64 bg-gradient-to-tl from-primary/10 to-transparent rounded-full opacity-0 group-hover:opacity-100 transition-all duration-500 group-hover:-translate-y-10 group-hover:-translate-x-10" />
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="mt-20 rounded-xl overflow-hidden border bg-background/50 backdrop-blur-sm"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
>
|
||||
<div className="grid md:grid-cols-2 gap-0">
|
||||
<div className="p-8 md:p-12 flex flex-col justify-center">
|
||||
<div className="inline-block rounded-full bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary mb-4 w-fit">
|
||||
{featured.label}
|
||||
</div>
|
||||
<h3 className="text-2xl md:text-3xl font-bold mb-4">
|
||||
{featured.title}
|
||||
</h3>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
{featured.description}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Button
|
||||
className="bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90 font-bold text-black hover:text-white cursor-pointer"
|
||||
onClick={() =>
|
||||
window.open(featured.primaryButtonLink, '_blank')
|
||||
}
|
||||
>
|
||||
{featured.primaryButtonText}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
window.open(featured.secondaryButtonLink, '_blank')
|
||||
}
|
||||
>
|
||||
{featured.secondaryButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative h-64 md:h-auto">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/20 to-blue-400/20" />
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-16 h-16 rounded-full bg-background/80 backdrop-blur-sm flex items-center justify-center cursor-pointer hover:bg-background transition-colors">
|
||||
<div className="w-0 h-0 border-t-8 border-t-transparent border-l-12 border-l-primary border-b-8 border-b-transparent ml-1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Button,
|
||||
MoonIcon,
|
||||
SunIcon,
|
||||
} from '@imphnen-frontend-service/shadcn-ui/atoms';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function SimpleThemeToggle() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(theme === 'dark' ? 'light' : 'dark');
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={toggleTheme}
|
||||
className="focus-visible:ring-0 cursor-pointer"
|
||||
>
|
||||
{theme === 'dark' ? (
|
||||
<SunIcon className="h-[1.2rem] w-[1.2rem]" />
|
||||
) : (
|
||||
<MoonIcon className="h-[1.2rem] w-[1.2rem]" />
|
||||
)}
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
'use client';
|
||||
|
||||
import { QuoteIcon } from '@imphnen-frontend-service/shadcn-ui/atoms';
|
||||
import { formatCMSImageDataToMedia } from 'apps/landing/src/lib/format';
|
||||
import { TestimonialsSection } from 'apps/landing/src/payload-types';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import Image from 'next/image';
|
||||
import { useRef } from 'react';
|
||||
|
||||
export function Testimonials(props: TestimonialsSection) {
|
||||
const { title, subtitle, items, stats, joinTitle, joinText } = props;
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const isInView = useInView(ref, { once: true, amount: 0.2 });
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: { opacity: 1, transition: { staggerChildren: 0.1 } },
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: { y: 0, opacity: 1, transition: { duration: 0.5 } },
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
id="testimoni"
|
||||
className="w-full py-20 md:py-32 bg-muted relative overflow-hidden"
|
||||
ref={ref}
|
||||
>
|
||||
<div className="absolute inset-0 -z-10">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,rgba(59,130,246,0.1),transparent_50%)]" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom,rgba(96,165,250,0.1),transparent_50%)]" />
|
||||
</div>
|
||||
|
||||
<div className="container px-4 md:px-6">
|
||||
<motion.div
|
||||
className="flex flex-col items-center justify-center space-y-4 text-center mb-16"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<h2 className="text-3xl font-bold tracking-tighter md:text-4xl lg:text-5xl">
|
||||
{title.split(' ').map((word, i) =>
|
||||
i === title.split(' ').length - 1 ? (
|
||||
<span
|
||||
key={i}
|
||||
className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400"
|
||||
>
|
||||
{word}
|
||||
</span>
|
||||
) : (
|
||||
<span key={i}>{word} </span>
|
||||
)
|
||||
)}
|
||||
</h2>
|
||||
<p className="max-w-[800px] mx-auto text-muted-foreground md:text-lg">
|
||||
{subtitle}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="grid gap-8 md:grid-cols-3"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
>
|
||||
{items?.map((t, idx) => (
|
||||
<motion.div
|
||||
key={idx}
|
||||
className="group relative overflow-hidden rounded-xl border bg-background p-6 transition-all hover:shadow-lg"
|
||||
variants={itemVariants}
|
||||
>
|
||||
<div className="absolute top-6 right-6 text-primary/20 group-hover:text-primary/40 transition-colors">
|
||||
<QuoteIcon className="h-8 w-8" />
|
||||
</div>
|
||||
<div className="relative z-10">
|
||||
<p className="mb-6 text-muted-foreground italic">
|
||||
“{t.quote}”
|
||||
</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative h-12 w-12 overflow-hidden rounded-full border-2 border-primary/20">
|
||||
{(() => {
|
||||
const media = formatCMSImageDataToMedia(t.avatar);
|
||||
if (!media) return null;
|
||||
return (
|
||||
<Image
|
||||
src={media.url!}
|
||||
alt={media.alt}
|
||||
width={600}
|
||||
height={500}
|
||||
className="object-cover"
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-bold">{t.name}</h4>
|
||||
<p className="text-sm text-muted-foreground">{t.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -bottom-1 -left-1 w-20 h-20 bg-gradient-to-tr from-primary/10 to-transparent rounded-tr-full opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="mt-20 rounded-xl overflow-hidden border bg-background/50 backdrop-blur-sm p-8 md:p-12"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
>
|
||||
<div className="grid md:grid-cols-2 gap-8 items-center">
|
||||
<div>
|
||||
<h3 className="text-2xl md:text-3xl font-bold mb-4">
|
||||
{joinTitle}
|
||||
</h3>
|
||||
<p className="text-muted-foreground">{joinText}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{stats?.map((s, idx) => (
|
||||
<div key={idx} className="rounded-lg border p-4 text-center">
|
||||
<div className="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
||||
{s.value}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">{s.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import type { ThemeProviderProps } from 'next-themes';
|
||||
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { type Metadata } from 'next';
|
||||
import { Inter } from 'next/font/google';
|
||||
import '../../styles/globals.css';
|
||||
import Footer from './_components/footer';
|
||||
import { Header } from './_components/header';
|
||||
import { ThemeProvider } from './_components/theme-provider';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'IMPHNEN - Ingin Menjadi Programmer Handal?',
|
||||
description: 'Komunitas belajar programming untuk semua level',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="id" suppressHydrationWarning>
|
||||
<body className={inter.className}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<Header />
|
||||
<main className="flex-1">{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { getGlobalsCallToActionSection } from '../../services/get-globals-call-to-action-section';
|
||||
import { getGlobalsCommunitiesSection } from '../../services/get-globals-communitues-section';
|
||||
import { getGlobalsFeaturesSection } from '../../services/get-globals-features-section';
|
||||
import { getGlobalsHeroSection } from '../../services/get-globals-hero-section';
|
||||
import { getGlobalsLearningResourcesSection } from '../../services/get-globals-learning-resources-section';
|
||||
import { getGlobalsTestimonialsSection } from '../../services/get-globals-testimonials-section';
|
||||
import { CallToAction } from './_components/call-to-action';
|
||||
import { Community } from './_components/community';
|
||||
import { Features } from './_components/features';
|
||||
import { Hero } from './_components/hero';
|
||||
import { LearningResources } from './_components/learning-resources';
|
||||
import { Testimonials } from './_components/testimonials';
|
||||
|
||||
export const revalidate = 10; // Seconds
|
||||
|
||||
export default async function Page() {
|
||||
const [
|
||||
heroData,
|
||||
featuresData,
|
||||
communitiesData,
|
||||
learningResourcesData,
|
||||
testimonialsData,
|
||||
callToActionData,
|
||||
] = await Promise.all([
|
||||
getGlobalsHeroSection(),
|
||||
getGlobalsFeaturesSection(),
|
||||
getGlobalsCommunitiesSection(),
|
||||
getGlobalsLearningResourcesSection(),
|
||||
getGlobalsTestimonialsSection(),
|
||||
getGlobalsCallToActionSection(),
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Hero {...heroData} />
|
||||
<Features {...featuresData} />
|
||||
<Community {...communitiesData} />
|
||||
<LearningResources {...learningResourcesData} />
|
||||
<Testimonials {...testimonialsData} />
|
||||
<CallToAction {...callToActionData} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views';
|
||||
import config from '../../../../payload.config';
|
||||
import { importMap } from '../importMap';
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
segments: string[];
|
||||
}>;
|
||||
searchParams: Promise<{
|
||||
[key: string]: string | string[];
|
||||
}>;
|
||||
};
|
||||
|
||||
export const generateMetadata = ({
|
||||
params,
|
||||
searchParams,
|
||||
}: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams });
|
||||
|
||||
const NotFound = ({ params, searchParams }: Args) =>
|
||||
NotFoundPage({ config, params, searchParams, importMap });
|
||||
|
||||
export default NotFound;
|
||||
@@ -0,0 +1,27 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { RootPage, generatePageMetadata } from '@payloadcms/next/views';
|
||||
import config from '../../../../payload.config';
|
||||
import { importMap } from '../importMap';
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
segments: string[];
|
||||
}>;
|
||||
searchParams: Promise<{
|
||||
[key: string]: string | string[];
|
||||
}>;
|
||||
};
|
||||
|
||||
export const generateMetadata = ({
|
||||
params,
|
||||
searchParams,
|
||||
}: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams });
|
||||
|
||||
const Page = ({ params, searchParams }: Args) =>
|
||||
RootPage({ config, params, searchParams, importMap });
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,5 @@
|
||||
import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client'
|
||||
|
||||
export const importMap = {
|
||||
"@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import '@payloadcms/next/css';
|
||||
import {
|
||||
REST_DELETE,
|
||||
REST_GET,
|
||||
REST_OPTIONS,
|
||||
REST_PATCH,
|
||||
REST_POST,
|
||||
REST_PUT,
|
||||
} from '@payloadcms/next/routes';
|
||||
import config from '../../../../payload.config';
|
||||
|
||||
export const GET = REST_GET(config);
|
||||
export const POST = REST_POST(config);
|
||||
export const DELETE = REST_DELETE(config);
|
||||
export const PATCH = REST_PATCH(config);
|
||||
export const PUT = REST_PUT(config);
|
||||
export const OPTIONS = REST_OPTIONS(config);
|
||||
@@ -0,0 +1,7 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import '@payloadcms/next/css';
|
||||
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes';
|
||||
import config from '../../../../payload.config';
|
||||
|
||||
export const GET = GRAPHQL_PLAYGROUND_GET(config);
|
||||
@@ -0,0 +1,8 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes';
|
||||
import config from '../../../../payload.config';
|
||||
|
||||
export const POST = GRAPHQL_POST(config);
|
||||
|
||||
export const OPTIONS = REST_OPTIONS(config);
|
||||
@@ -0,0 +1,35 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import '@payloadcms/next/css';
|
||||
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts';
|
||||
import type { ServerFunctionClient } from 'payload';
|
||||
import React from 'react';
|
||||
import config from '../../payload.config';
|
||||
|
||||
import { importMap } from './admin/importMap.js';
|
||||
import './custom.scss';
|
||||
|
||||
type Args = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const serverFunction: ServerFunctionClient = async function (args) {
|
||||
'use server';
|
||||
return handleServerFunctions({
|
||||
...args,
|
||||
config,
|
||||
importMap,
|
||||
});
|
||||
};
|
||||
|
||||
const Layout = ({ children }: Args) => (
|
||||
<RootLayout
|
||||
config={config}
|
||||
importMap={importMap}
|
||||
serverFunction={serverFunction}
|
||||
>
|
||||
{children}
|
||||
</RootLayout>
|
||||
);
|
||||
|
||||
export default Layout;
|
||||
@@ -0,0 +1,51 @@
|
||||
import { GlobalConfig } from 'payload';
|
||||
|
||||
export const CallToActionSection: GlobalConfig = {
|
||||
slug: 'call-to-action-section',
|
||||
label: 'Call To Action Section',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
defaultValue: 'Siap Menjadi',
|
||||
},
|
||||
{
|
||||
name: 'highlightedTitle',
|
||||
type: 'text',
|
||||
required: true,
|
||||
defaultValue: 'Programmer Handal?',
|
||||
},
|
||||
{
|
||||
name: 'subtitle',
|
||||
type: 'textarea',
|
||||
required: true,
|
||||
defaultValue:
|
||||
'Bergabunglah dengan komunitas IMPHNEN sekarang dan mulai perjalanan programming mu dengan cara yang menyenangkan!',
|
||||
},
|
||||
{
|
||||
name: 'primaryButtonLabel',
|
||||
type: 'text',
|
||||
required: true,
|
||||
defaultValue: 'Gabung Discord',
|
||||
},
|
||||
{
|
||||
name: 'primaryButtonLink',
|
||||
type: 'text',
|
||||
required: true,
|
||||
defaultValue: 'https://discord.gg/imphnen',
|
||||
},
|
||||
{
|
||||
name: 'secondaryButtonLabel',
|
||||
type: 'text',
|
||||
required: true,
|
||||
defaultValue: 'Join Facebook Group',
|
||||
},
|
||||
{
|
||||
name: 'secondaryButtonLink',
|
||||
type: 'text',
|
||||
required: true,
|
||||
defaultValue: 'https://facebook.com/groups/programmerhandal',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import { GlobalConfig } from 'payload';
|
||||
|
||||
export const CommunitiesSection: GlobalConfig = {
|
||||
slug: 'communities-section',
|
||||
label: 'Communities Section',
|
||||
fields: [
|
||||
{
|
||||
name: 'items',
|
||||
type: 'array',
|
||||
required: true,
|
||||
defaultValue: [
|
||||
{
|
||||
iconName: 'tabler:brand-facebook',
|
||||
title: 'Facebook Group',
|
||||
description:
|
||||
'Bergabunglah dengan grup Facebook kami untuk diskusi santai dan berbagi artikel menarik.',
|
||||
buttonText: 'Gabung Sekarang',
|
||||
buttonLink: 'https://facebook.com/groups/programmerhandal',
|
||||
},
|
||||
{
|
||||
iconName: 'tabler:brand-instagram',
|
||||
title: 'Instagram',
|
||||
description:
|
||||
'Ikuti kami di Instagram untuk tips programming, konten inspiratif, dan info event terbaru.',
|
||||
buttonText: 'Follow Kami',
|
||||
buttonLink: 'https://www.instagram.com/imphnen.dev',
|
||||
},
|
||||
{
|
||||
iconName: 'tabler:brand-discord-filled',
|
||||
title: 'Discord Server',
|
||||
description:
|
||||
'Diskusikan langsung dengan sesama programmer dan dapatkan bantuan langsung dari para ahli.',
|
||||
buttonText: 'Join Server',
|
||||
buttonLink: 'https://discord.com/invite/imphnen',
|
||||
},
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
name: 'iconName',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{ name: 'title', type: 'text', required: true },
|
||||
{ name: 'description', type: 'textarea', required: true },
|
||||
{ name: 'buttonText', type: 'text', required: true },
|
||||
{ name: 'buttonLink', type: 'text', required: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'stats',
|
||||
type: 'array',
|
||||
required: true,
|
||||
defaultValue: [
|
||||
{ value: '100K+', label: 'Member Aktif' },
|
||||
{ value: '50+', label: 'Event Bulanan' },
|
||||
{ value: '100+', label: 'Mentor Profesional' },
|
||||
{ value: '5K+', label: 'Diskusi Mingguan' },
|
||||
],
|
||||
fields: [
|
||||
{ name: 'value', type: 'text', required: true },
|
||||
{ name: 'label', type: 'text', required: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import { GlobalConfig } from 'payload';
|
||||
|
||||
export const FeaturesSection: GlobalConfig = {
|
||||
slug: 'features-section',
|
||||
label: 'Features Section',
|
||||
fields: [
|
||||
{
|
||||
name: 'heading',
|
||||
type: 'text',
|
||||
defaultValue: 'Belajar programming dengan cara yang lebih baik',
|
||||
},
|
||||
{
|
||||
name: 'subheading',
|
||||
type: 'text',
|
||||
defaultValue:
|
||||
'IMPHNEN hadir dengan berbagai fitur untuk membantu kamu menjadi programmer handal tanpa harus pusing dengan coding.',
|
||||
},
|
||||
{
|
||||
name: 'features',
|
||||
type: 'array',
|
||||
defaultValue: [
|
||||
{
|
||||
iconName: 'tabler:device-laptop',
|
||||
title: 'Belajar Tanpa Koding',
|
||||
description:
|
||||
'Pelajari konsep programming dengan cara yang mudah dipahami tanpa harus menulis kode yang rumit.',
|
||||
},
|
||||
{
|
||||
iconName: 'tabler:users',
|
||||
title: 'Komunitas Supportif',
|
||||
description:
|
||||
'Bergabunglah dengan komunitas programmer Indonesia yang siap membantu dan berbagi pengalaman.',
|
||||
},
|
||||
{
|
||||
iconName: 'tabler:book',
|
||||
title: 'Tutorial Interaktif',
|
||||
description:
|
||||
'Akses tutorial interaktif yang membuat konsep programming lebih mudah untuk dipahami.',
|
||||
},
|
||||
{
|
||||
iconName: 'tabler:code',
|
||||
title: 'Proyek Praktis',
|
||||
description:
|
||||
'Terapkan pengetahuan Anda dalam proyek nyata dengan panduan langkah demi langkah.',
|
||||
},
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
name: 'iconName',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
import type { GlobalConfig } from 'payload';
|
||||
|
||||
export const HeroSection: GlobalConfig = {
|
||||
slug: 'hero-section',
|
||||
label: 'Hero Section',
|
||||
fields: [
|
||||
{
|
||||
name: 'heroImage',
|
||||
label: 'Hero Image',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'badgeText',
|
||||
label: 'Badge Text',
|
||||
type: 'text',
|
||||
required: true,
|
||||
defaultValue: 'Komunitas Programmer Indonesia',
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
label: 'Main Title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
defaultValue: 'Programmer Handal,',
|
||||
},
|
||||
{
|
||||
name: 'highlight',
|
||||
label: 'Highlight Text',
|
||||
type: 'text',
|
||||
required: true,
|
||||
defaultValue: 'Tanpa Ribet',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
type: 'textarea',
|
||||
required: true,
|
||||
defaultValue:
|
||||
'Temukan potensi programming Anda bersama komunitas yang mendukung, tutorial interaktif, dan sumber daya berkualitas tinggi.',
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
name: 'buttons',
|
||||
label: 'Buttons',
|
||||
fields: [
|
||||
{
|
||||
name: 'primaryLabel',
|
||||
label: 'Primary Button Label',
|
||||
type: 'text',
|
||||
required: true,
|
||||
defaultValue: 'Mulai Belajar',
|
||||
},
|
||||
{
|
||||
name: 'primaryUrl',
|
||||
label: 'Primary Button URL',
|
||||
type: 'text',
|
||||
required: true,
|
||||
defaultValue: 'https://facebook.com/groups/programmerhandal',
|
||||
},
|
||||
{
|
||||
name: 'secondaryLabel',
|
||||
label: 'Secondary Button Label',
|
||||
type: 'text',
|
||||
required: true,
|
||||
defaultValue: 'Gabung Discord',
|
||||
},
|
||||
{
|
||||
name: 'secondaryUrl',
|
||||
label: 'Secondary Button URL',
|
||||
type: 'text',
|
||||
required: true,
|
||||
defaultValue: 'https://discord.com/invite/imphnen',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'stats',
|
||||
label: 'Statistics',
|
||||
type: 'array',
|
||||
minRows: 1,
|
||||
fields: [
|
||||
{
|
||||
name: 'value',
|
||||
label: 'Value',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'label',
|
||||
label: 'Label',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
defaultValue: [
|
||||
{ value: '180K+', label: 'Member' },
|
||||
{ value: '500+', label: 'Tutorial' },
|
||||
{ value: '24/7', label: 'Yapping' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
// payload config
|
||||
import { GlobalConfig } from 'payload';
|
||||
|
||||
export const LearningResourcesSection: GlobalConfig = {
|
||||
slug: 'learning-resources-section',
|
||||
label: 'Learning Resources Section',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
defaultValue: 'Sumber Belajar',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
required: true,
|
||||
defaultValue:
|
||||
'Akses berbagai materi belajar yang akan membantu kamu menguasai konsep programming dengan cara yang menyenangkan.',
|
||||
},
|
||||
{
|
||||
name: 'resources',
|
||||
type: 'array',
|
||||
required: true,
|
||||
fields: [
|
||||
{ name: 'icon', type: 'text', required: true },
|
||||
{ name: 'title', type: 'text', required: true },
|
||||
{ name: 'description', type: 'textarea', required: true },
|
||||
{ name: 'buttonText', type: 'text', required: true },
|
||||
{ name: 'buttonLink', type: 'text', required: true },
|
||||
],
|
||||
defaultValue: [
|
||||
{
|
||||
icon: 'tabler:video',
|
||||
title: 'Video Tutorial',
|
||||
description:
|
||||
'Belajar melalui tutorial video dari langkah awal hingga mahir.',
|
||||
buttonText: 'Lihat Semua Video',
|
||||
buttonLink: '#',
|
||||
},
|
||||
{
|
||||
icon: 'tabler:article',
|
||||
title: 'Artikel & Tutorial',
|
||||
description:
|
||||
'Pelajari konsep programming melalui artikel yang disusun secara terstruktur.',
|
||||
buttonText: 'Baca Artikel',
|
||||
buttonLink: '#',
|
||||
},
|
||||
{
|
||||
icon: 'tabler:brand-vscode',
|
||||
title: 'Tantangan Koding',
|
||||
description:
|
||||
'Uji kemampuan koding kamu dengan tantangan yang menyenangkan dan menantang.',
|
||||
buttonText: 'Mulai Tantangan',
|
||||
buttonLink: '#',
|
||||
},
|
||||
{
|
||||
icon: 'tabler:device-desktop-share',
|
||||
title: 'Sharing Session',
|
||||
description:
|
||||
'Ikuti sesi berbagi pengalaman dari programmer berpengalaman dan belajar dari pengalaman mereka.',
|
||||
buttonText: 'Jadwal Session',
|
||||
buttonLink: '#',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'featured',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'label',
|
||||
type: 'text',
|
||||
required: true,
|
||||
defaultValue: 'Rekomendasi Terbaik',
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
defaultValue: 'Kursus Lengkap Web Development',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
required: true,
|
||||
defaultValue:
|
||||
'Pelajari HTML, CSS, JavaScript, React, dan Node.js dalam satu kursus komprehensif yang dirancang untuk pemula hingga tingkat menengah.',
|
||||
},
|
||||
{
|
||||
name: 'primaryButtonText',
|
||||
type: 'text',
|
||||
required: true,
|
||||
defaultValue: 'Mulai Kursus',
|
||||
},
|
||||
{
|
||||
name: 'primaryButtonLink',
|
||||
type: 'text',
|
||||
required: true,
|
||||
defaultValue: '#',
|
||||
},
|
||||
{
|
||||
name: 'secondaryButtonText',
|
||||
type: 'text',
|
||||
required: true,
|
||||
defaultValue: 'Lihat Silabus',
|
||||
},
|
||||
{
|
||||
name: 'secondaryButtonLink',
|
||||
type: 'text',
|
||||
required: true,
|
||||
defaultValue: '#',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { CollectionConfig } from 'payload';
|
||||
|
||||
export const Media: CollectionConfig = {
|
||||
slug: 'media',
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'alt',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
upload: true,
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
import { GlobalConfig } from 'payload';
|
||||
|
||||
export const TestimonialsSection: GlobalConfig = {
|
||||
slug: 'testimonials-section',
|
||||
label: 'Testimonials Section',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
label: 'Section Title',
|
||||
type: 'text',
|
||||
defaultValue: 'Testimoni Member',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'subtitle',
|
||||
label: 'Section Subtitle',
|
||||
type: 'textarea',
|
||||
defaultValue:
|
||||
'Apa kata mereka yang telah bergabung dengan komunitas IMPHNEN?',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'items',
|
||||
label: 'Testimonials Items',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{ name: 'quote', type: 'textarea', label: 'Quote', required: true },
|
||||
{ name: 'name', type: 'text', label: 'Name', required: true },
|
||||
{ name: 'role', type: 'text', label: 'Role', required: true },
|
||||
{
|
||||
name: 'avatar',
|
||||
type: 'upload',
|
||||
label: 'Avatar',
|
||||
relationTo: 'media',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
defaultValue: [
|
||||
{
|
||||
quote:
|
||||
'Saya yang tadinya tidak mengerti apa-apa tentang programming, sekarang bisa membuat website sendiri dengan mudah. Terima kasih IMPHNEN!',
|
||||
name: 'Budi Santoso',
|
||||
role: 'Web Developer Pemula',
|
||||
avatar: { id: '', relationTo: 'media' },
|
||||
},
|
||||
{
|
||||
quote:
|
||||
'Komunitas yang sangat supportif! Setiap pertanyaan selalu dijawab dengan cepat dan jelas. Diskusi programnya mudah dipahami.',
|
||||
name: 'Anita Ratna',
|
||||
role: 'Mobile App Developer',
|
||||
avatar: { id: '', relationTo: 'media' },
|
||||
},
|
||||
{
|
||||
quote:
|
||||
'Server Discord IMPHNEN adalah tempat belajar terbaik untuk programmer pemula seperti saya. Materinya lengkap dan komunitasnya sangat membantu!',
|
||||
name: 'Dedi Permana',
|
||||
role: 'Data Scientist',
|
||||
avatar: { id: '', relationTo: 'media' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'stats',
|
||||
label: 'Statistics',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{ name: 'value', type: 'text', label: 'Value', required: true },
|
||||
{ name: 'label', type: 'text', label: 'Label', required: true },
|
||||
],
|
||||
defaultValue: [
|
||||
{ value: '98%', label: 'Tingkat Kemalasan' },
|
||||
{ value: '4.9/5', label: 'Rating Drama' },
|
||||
{ value: '85%', label: 'Mendapat Pekerjaan' },
|
||||
{ value: '24/7', label: 'Yapping' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'joinTitle',
|
||||
label: 'Join Section Title',
|
||||
type: 'text',
|
||||
defaultValue: 'Bergabunglah dengan 10,000+ programmer Indonesia lainnya',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'joinText',
|
||||
label: 'Join Section Text',
|
||||
type: 'textarea',
|
||||
defaultValue:
|
||||
'Komunitas kami terus berkembang dengan programmer dari berbagai latar belakang dan tingkat keahlian. Bersama-sama, kita belajar, berbagi, dan tumbuh sebagai profesional.',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { CollectionConfig } from 'payload';
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
admin: {
|
||||
useAsTitle: 'email',
|
||||
},
|
||||
auth: true,
|
||||
fields: [
|
||||
// Email added by default
|
||||
// Add more fields as needed
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Media } from '../payload-types';
|
||||
|
||||
export function formatCMSImageDataToMedia(
|
||||
image: number | Media | null | undefined
|
||||
): Media | null {
|
||||
if (image && typeof image === 'object' && 'url' in image) {
|
||||
return image as Media;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { getPayload } from 'payload';
|
||||
import config from '../payload.config';
|
||||
|
||||
export const payload = await getPayload({ config });
|
||||
@@ -0,0 +1,591 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Supported timezones in IANA format.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "supportedTimezones".
|
||||
*/
|
||||
export type SupportedTimezones =
|
||||
| 'Pacific/Midway'
|
||||
| 'Pacific/Niue'
|
||||
| 'Pacific/Honolulu'
|
||||
| 'Pacific/Rarotonga'
|
||||
| 'America/Anchorage'
|
||||
| 'Pacific/Gambier'
|
||||
| 'America/Los_Angeles'
|
||||
| 'America/Tijuana'
|
||||
| 'America/Denver'
|
||||
| 'America/Phoenix'
|
||||
| 'America/Chicago'
|
||||
| 'America/Guatemala'
|
||||
| 'America/New_York'
|
||||
| 'America/Bogota'
|
||||
| 'America/Caracas'
|
||||
| 'America/Santiago'
|
||||
| 'America/Buenos_Aires'
|
||||
| 'America/Sao_Paulo'
|
||||
| 'Atlantic/South_Georgia'
|
||||
| 'Atlantic/Azores'
|
||||
| 'Atlantic/Cape_Verde'
|
||||
| 'Europe/London'
|
||||
| 'Europe/Berlin'
|
||||
| 'Africa/Lagos'
|
||||
| 'Europe/Athens'
|
||||
| 'Africa/Cairo'
|
||||
| 'Europe/Moscow'
|
||||
| 'Asia/Riyadh'
|
||||
| 'Asia/Dubai'
|
||||
| 'Asia/Baku'
|
||||
| 'Asia/Karachi'
|
||||
| 'Asia/Tashkent'
|
||||
| 'Asia/Calcutta'
|
||||
| 'Asia/Dhaka'
|
||||
| 'Asia/Almaty'
|
||||
| 'Asia/Jakarta'
|
||||
| 'Asia/Bangkok'
|
||||
| 'Asia/Shanghai'
|
||||
| 'Asia/Singapore'
|
||||
| 'Asia/Tokyo'
|
||||
| 'Asia/Seoul'
|
||||
| 'Australia/Brisbane'
|
||||
| 'Australia/Sydney'
|
||||
| 'Pacific/Guam'
|
||||
| 'Pacific/Noumea'
|
||||
| 'Pacific/Auckland'
|
||||
| 'Pacific/Fiji';
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
users: User;
|
||||
media: Media;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
media: MediaSelect<false> | MediaSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: number;
|
||||
};
|
||||
globals: {
|
||||
'hero-section': HeroSection;
|
||||
'features-section': FeaturesSection;
|
||||
'communities-section': CommunitiesSection;
|
||||
'learning-resources-section': LearningResourcesSection;
|
||||
'testimonials-section': TestimonialsSection;
|
||||
'call-to-action-section': CallToActionSection;
|
||||
};
|
||||
globalsSelect: {
|
||||
'hero-section': HeroSectionSelect<false> | HeroSectionSelect<true>;
|
||||
'features-section': FeaturesSectionSelect<false> | FeaturesSectionSelect<true>;
|
||||
'communities-section': CommunitiesSectionSelect<false> | CommunitiesSectionSelect<true>;
|
||||
'learning-resources-section': LearningResourcesSectionSelect<false> | LearningResourcesSectionSelect<true>;
|
||||
'testimonials-section': TestimonialsSectionSelect<false> | TestimonialsSectionSelect<true>;
|
||||
'call-to-action-section': CallToActionSectionSelect<false> | CallToActionSectionSelect<true>;
|
||||
};
|
||||
locale: null;
|
||||
user: User & {
|
||||
collection: 'users';
|
||||
};
|
||||
jobs: {
|
||||
tasks: unknown;
|
||||
workflows: unknown;
|
||||
};
|
||||
}
|
||||
export interface UserAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
login: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
unlock: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: number;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string | null;
|
||||
resetPasswordExpiration?: string | null;
|
||||
salt?: string | null;
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "media".
|
||||
*/
|
||||
export interface Media {
|
||||
id: number;
|
||||
alt: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
thumbnailURL?: string | null;
|
||||
filename?: string | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
focalX?: number | null;
|
||||
focalY?: number | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: number;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: number | User;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'media';
|
||||
value: number | Media;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: number | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: number;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: number | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: number;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users_select".
|
||||
*/
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
resetPasswordToken?: T;
|
||||
resetPasswordExpiration?: T;
|
||||
salt?: T;
|
||||
hash?: T;
|
||||
loginAttempts?: T;
|
||||
lockUntil?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "media_select".
|
||||
*/
|
||||
export interface MediaSelect<T extends boolean = true> {
|
||||
alt?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
url?: T;
|
||||
thumbnailURL?: T;
|
||||
filename?: T;
|
||||
mimeType?: T;
|
||||
filesize?: T;
|
||||
width?: T;
|
||||
height?: T;
|
||||
focalX?: T;
|
||||
focalY?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
*/
|
||||
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
|
||||
document?: T;
|
||||
globalSlug?: T;
|
||||
user?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences_select".
|
||||
*/
|
||||
export interface PayloadPreferencesSelect<T extends boolean = true> {
|
||||
user?: T;
|
||||
key?: T;
|
||||
value?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations_select".
|
||||
*/
|
||||
export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
batch?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "hero-section".
|
||||
*/
|
||||
export interface HeroSection {
|
||||
id: number;
|
||||
heroImage: number | Media;
|
||||
badgeText: string;
|
||||
title: string;
|
||||
highlight: string;
|
||||
description: string;
|
||||
buttons: {
|
||||
primaryLabel: string;
|
||||
primaryUrl: string;
|
||||
secondaryLabel: string;
|
||||
secondaryUrl: string;
|
||||
};
|
||||
stats?:
|
||||
| {
|
||||
value: string;
|
||||
label: string;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "features-section".
|
||||
*/
|
||||
export interface FeaturesSection {
|
||||
id: number;
|
||||
heading?: string | null;
|
||||
subheading?: string | null;
|
||||
features?:
|
||||
| {
|
||||
iconName: string;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "communities-section".
|
||||
*/
|
||||
export interface CommunitiesSection {
|
||||
id: number;
|
||||
items: {
|
||||
iconName: string;
|
||||
title: string;
|
||||
description: string;
|
||||
buttonText: string;
|
||||
buttonLink: string;
|
||||
id?: string | null;
|
||||
}[];
|
||||
stats: {
|
||||
value: string;
|
||||
label: string;
|
||||
id?: string | null;
|
||||
}[];
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "learning-resources-section".
|
||||
*/
|
||||
export interface LearningResourcesSection {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
resources: {
|
||||
icon: string;
|
||||
title: string;
|
||||
description: string;
|
||||
buttonText: string;
|
||||
buttonLink: string;
|
||||
id?: string | null;
|
||||
}[];
|
||||
featured: {
|
||||
label: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButtonText: string;
|
||||
primaryButtonLink: string;
|
||||
secondaryButtonText: string;
|
||||
secondaryButtonLink: string;
|
||||
};
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "testimonials-section".
|
||||
*/
|
||||
export interface TestimonialsSection {
|
||||
id: number;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
items?:
|
||||
| {
|
||||
quote: string;
|
||||
name: string;
|
||||
role: string;
|
||||
avatar: number | Media;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
stats?:
|
||||
| {
|
||||
value: string;
|
||||
label: string;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
joinTitle: string;
|
||||
joinText: string;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "call-to-action-section".
|
||||
*/
|
||||
export interface CallToActionSection {
|
||||
id: number;
|
||||
title: string;
|
||||
highlightedTitle: string;
|
||||
subtitle: string;
|
||||
primaryButtonLabel: string;
|
||||
primaryButtonLink: string;
|
||||
secondaryButtonLabel: string;
|
||||
secondaryButtonLink: string;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "hero-section_select".
|
||||
*/
|
||||
export interface HeroSectionSelect<T extends boolean = true> {
|
||||
heroImage?: T;
|
||||
badgeText?: T;
|
||||
title?: T;
|
||||
highlight?: T;
|
||||
description?: T;
|
||||
buttons?:
|
||||
| T
|
||||
| {
|
||||
primaryLabel?: T;
|
||||
primaryUrl?: T;
|
||||
secondaryLabel?: T;
|
||||
secondaryUrl?: T;
|
||||
};
|
||||
stats?:
|
||||
| T
|
||||
| {
|
||||
value?: T;
|
||||
label?: T;
|
||||
id?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "features-section_select".
|
||||
*/
|
||||
export interface FeaturesSectionSelect<T extends boolean = true> {
|
||||
heading?: T;
|
||||
subheading?: T;
|
||||
features?:
|
||||
| T
|
||||
| {
|
||||
iconName?: T;
|
||||
title?: T;
|
||||
description?: T;
|
||||
id?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "communities-section_select".
|
||||
*/
|
||||
export interface CommunitiesSectionSelect<T extends boolean = true> {
|
||||
items?:
|
||||
| T
|
||||
| {
|
||||
iconName?: T;
|
||||
title?: T;
|
||||
description?: T;
|
||||
buttonText?: T;
|
||||
buttonLink?: T;
|
||||
id?: T;
|
||||
};
|
||||
stats?:
|
||||
| T
|
||||
| {
|
||||
value?: T;
|
||||
label?: T;
|
||||
id?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "learning-resources-section_select".
|
||||
*/
|
||||
export interface LearningResourcesSectionSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
description?: T;
|
||||
resources?:
|
||||
| T
|
||||
| {
|
||||
icon?: T;
|
||||
title?: T;
|
||||
description?: T;
|
||||
buttonText?: T;
|
||||
buttonLink?: T;
|
||||
id?: T;
|
||||
};
|
||||
featured?:
|
||||
| T
|
||||
| {
|
||||
label?: T;
|
||||
title?: T;
|
||||
description?: T;
|
||||
primaryButtonText?: T;
|
||||
primaryButtonLink?: T;
|
||||
secondaryButtonText?: T;
|
||||
secondaryButtonLink?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "testimonials-section_select".
|
||||
*/
|
||||
export interface TestimonialsSectionSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
subtitle?: T;
|
||||
items?:
|
||||
| T
|
||||
| {
|
||||
quote?: T;
|
||||
name?: T;
|
||||
role?: T;
|
||||
avatar?: T;
|
||||
id?: T;
|
||||
};
|
||||
stats?:
|
||||
| T
|
||||
| {
|
||||
value?: T;
|
||||
label?: T;
|
||||
id?: T;
|
||||
};
|
||||
joinTitle?: T;
|
||||
joinText?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "call-to-action-section_select".
|
||||
*/
|
||||
export interface CallToActionSectionSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
highlightedTitle?: T;
|
||||
subtitle?: T;
|
||||
primaryButtonLabel?: T;
|
||||
primaryButtonLink?: T;
|
||||
secondaryButtonLabel?: T;
|
||||
secondaryButtonLink?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
*/
|
||||
export interface Auth {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres';
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical';
|
||||
import { s3Storage } from '@payloadcms/storage-s3';
|
||||
import { NodeHttpHandler } from '@smithy/node-http-handler';
|
||||
import https from 'https';
|
||||
import path from 'path';
|
||||
import { buildConfig } from 'payload';
|
||||
import sharp from 'sharp';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { CallToActionSection } from './collections/CallToActionSection';
|
||||
import { CommunitiesSection } from './collections/CommunitiesSection';
|
||||
import { FeaturesSection } from './collections/FeaturesSection';
|
||||
import { HeroSection } from './collections/HeroSection';
|
||||
import { LearningResourcesSection } from './collections/LearningResourcesSection';
|
||||
import { Media } from './collections/Media';
|
||||
import { TestimonialsSection } from './collections/TestimonialsSection';
|
||||
import { Users } from './collections/Users';
|
||||
|
||||
const filename = fileURLToPath(import.meta.url);
|
||||
const dirname = path.dirname(filename);
|
||||
|
||||
export default buildConfig({
|
||||
admin: {
|
||||
user: Users.slug,
|
||||
importMap: {
|
||||
importMapFile: path.resolve(
|
||||
__dirname,
|
||||
'app',
|
||||
'(payload)',
|
||||
'admin',
|
||||
'importMap.js'
|
||||
),
|
||||
},
|
||||
},
|
||||
collections: [Users, Media],
|
||||
globals: [
|
||||
HeroSection,
|
||||
FeaturesSection,
|
||||
CommunitiesSection,
|
||||
LearningResourcesSection,
|
||||
TestimonialsSection,
|
||||
CallToActionSection,
|
||||
],
|
||||
editor: lexicalEditor(),
|
||||
secret: process.env.CMS_SECRET || '',
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
db: postgresAdapter({
|
||||
pool: {
|
||||
connectionString: process.env.CMS_POSTGRES_URL || '',
|
||||
},
|
||||
}),
|
||||
sharp,
|
||||
plugins: [
|
||||
s3Storage({
|
||||
collections: {
|
||||
media: {
|
||||
disableLocalStorage: true,
|
||||
},
|
||||
},
|
||||
bucket: 'landing-cms',
|
||||
config: {
|
||||
endpoint: process.env.CMS_STORAGE_ENDPOINT,
|
||||
credentials: {
|
||||
accessKeyId: process.env.CMS_STORAGE_ACCESS_KEY_ID!,
|
||||
secretAccessKey: process.env.CMS_STORAGE_SECRET_ACCESS_KEY!,
|
||||
},
|
||||
region: process.env.CMS_STORAGE_REGION!,
|
||||
forcePathStyle: true,
|
||||
requestHandler: new NodeHttpHandler({
|
||||
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { payload } from '../lib/payload';
|
||||
|
||||
export async function getGlobalsCallToActionSection() {
|
||||
return await payload.findGlobal({
|
||||
slug: 'call-to-action-section',
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { payload } from '../lib/payload';
|
||||
|
||||
export async function getGlobalsCommunitiesSection() {
|
||||
return await payload.findGlobal({
|
||||
slug: 'communities-section',
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { payload } from '../lib/payload';
|
||||
|
||||
export async function getGlobalsFeaturesSection() {
|
||||
return await payload.findGlobal({
|
||||
slug: 'features-section',
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { payload } from '../lib/payload';
|
||||
|
||||
export async function getGlobalsHeroSection() {
|
||||
return await payload.findGlobal({
|
||||
slug: 'hero-section',
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { payload } from '../lib/payload';
|
||||
|
||||
export async function getGlobalsLearningResourcesSection() {
|
||||
return await payload.findGlobal({
|
||||
slug: 'learning-resources-section',
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { payload } from '../lib/payload';
|
||||
|
||||
export async function getGlobalsTestimonialsSection() {
|
||||
return await payload.findGlobal({
|
||||
slug: 'testimonials-section',
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@import 'tailwindcss';
|
||||
@import "../../../../libs/shadcn-ui/src/index.css";
|
||||
@source "../../../../libs/shadcn-ui/src/atoms/**/*.{ts,tsx}";
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"emitDeclarationOnly": false,
|
||||
"esModuleInterop": true,
|
||||
"target": "ES2022",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.js",
|
||||
"**/*.jsx",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"../../apps/landing/.next/types/**/*.ts",
|
||||
"../../dist/apps/landing/.next/types/**/*.ts",
|
||||
"next-env.d.ts",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"jest.config.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.ts"
|
||||
]
|
||||
}
|
||||
+1
-1
@@ -12,7 +12,7 @@ export default [
|
||||
],
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'],
|
||||
rules: {
|
||||
'@nx/enforce-module-boundaries': [
|
||||
'error',
|
||||
|
||||
@@ -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({
|
||||
@@ -40,24 +40,6 @@ export const authRegisterSchema = z
|
||||
.min(1, 'Password tidak boleh kosong')
|
||||
.min(8, 'Password harus lebih dari 8 karakter')
|
||||
.max(50, 'Password tidak boleh lebih dari 50 karakter'),
|
||||
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(),
|
||||
confirm_password: z
|
||||
.string({
|
||||
required_error: 'Konfirmasi password tidak boleh kosong',
|
||||
@@ -65,12 +47,32 @@ export const authRegisterSchema = z
|
||||
.min(1, 'Konfirmasi password tidak boleh kosong')
|
||||
.max(50, 'Konfirmasi password tidak boleh lebih dari 50 karakter'),
|
||||
})
|
||||
.superRefine((val, ctx) => {
|
||||
if (val.password !== val.confirm_password) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['confirm_password'],
|
||||
message: `No duplicates allowed.`,
|
||||
});
|
||||
}
|
||||
.refine((data) => data.password === data.confirm_password, {
|
||||
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
|
||||
);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const gachaItemSchema = z.object({
|
||||
itemName: z
|
||||
.string({
|
||||
required_error: 'Nama item tidak boleh kosong',
|
||||
invalid_type_error: 'Nama item harus berupa string',
|
||||
})
|
||||
.min(1, 'Nama item tidak boleh kosong'),
|
||||
quantity: z
|
||||
.number({
|
||||
required_error: 'Quantity tidak boleh kosong',
|
||||
invalid_type_error: 'Quantity harus berupa angka',
|
||||
})
|
||||
.min(1, 'Quantity paling sedikit adalah 1'),
|
||||
foto: z
|
||||
.instanceof(File)
|
||||
.optional()
|
||||
.refine(
|
||||
(file) => !file || file.size <= 5000000, // 5MB in bytes
|
||||
'Ukuran file maksimal 5MB'
|
||||
)
|
||||
.refine(
|
||||
(file) => !file || ['image/jpeg', 'image/png', 'image/webp'].includes(file.type),
|
||||
'Format file harus JPG, PNG, atau WEBP'
|
||||
)
|
||||
});
|
||||
|
||||
export const gachaRollItemSchema = z.object({
|
||||
itemName: z
|
||||
.string({
|
||||
required_error: 'Nama item tidak boleh kosong',
|
||||
invalid_type_error: 'Nama item harus berupa string',
|
||||
})
|
||||
.min(1, 'Nama item tidak boleh kosong'),
|
||||
quantity: z
|
||||
.number({
|
||||
required_error: 'Quantity tidak boleh kosong',
|
||||
invalid_type_error: 'Quantity harus berupa angka',
|
||||
})
|
||||
.min(1, 'Quantity paling sedikit adalah 1'),
|
||||
chanceRate: z
|
||||
.number({
|
||||
required_error: 'Chance rate tidak boleh kosong',
|
||||
invalid_type_error: 'Chance rate harus berupa angka',
|
||||
})
|
||||
.min(0.1, 'Chance rate paling sedikit adalah 0,1')
|
||||
.max(1, 'Chance rate paling banyak adalah 1'),
|
||||
});
|
||||
@@ -1 +1,2 @@
|
||||
export * from './auth';
|
||||
export * from './gacha';
|
||||
|
||||
@@ -1 +1,11 @@
|
||||
export {};
|
||||
export type TGachaItem = {
|
||||
itemName: string;
|
||||
quantity: number;
|
||||
foto?: File;
|
||||
};
|
||||
|
||||
export type TGachaRollItem = {
|
||||
itemName: string;
|
||||
quantity: number;
|
||||
chanceRate: number;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@nx/react/babel",
|
||||
{
|
||||
"runtime": "automatic",
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": []
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# shadcn-ui
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test shadcn-ui` to execute the unit tests via [Vitest](https://vitest.dev/).
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@imphnen-frontend-service/service/components",
|
||||
"utils": "@imphnen-frontend-service/service/lib/utils",
|
||||
"ui": "@imphnen-frontend-service/service/components/ui",
|
||||
"lib": "@imphnen-frontend-service/service/lib",
|
||||
"hooks": "@imphnen-frontend-service/service/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
Generated
+1654
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@imphnen-frontend-service/shadcn-ui",
|
||||
"version": "0.0.1",
|
||||
"main": "./index.js",
|
||||
"types": "./index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./index.mjs",
|
||||
"require": "./index.js"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.2.0",
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.503.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss": "^4.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tw-animate-css": "^1.2.8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "shadcn-ui",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/shadcn-ui/src",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"// targets": "to see all targets run: nx show project shadcn-ui --web",
|
||||
"targets": {}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@imphnen-frontend-service/utils';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import * as React from 'react';
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||
outline:
|
||||
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
icon: 'h-9 w-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Button.displayName = 'Button';
|
||||
|
||||
export { Button, buttonVariants };
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user