Compare commits

..
Author SHA1 Message Date
ArraysID 4f394bacf1 chore: remove pnpm, accidentally added out of habit 2025-04-27 06:22:02 +07:00
ArraysID d4bd7f4e1a feat: initialize index file to export atoms 2025-04-27 06:16:32 +07:00
ArraysID 1b6b442078 chore: add tailwind-animate-css package 2025-04-27 06:14:43 +07:00
ArraysID f9191708f4 feat: update dependencies in package.json and package-lock.json 2025-04-27 06:11:39 +07:00
ArraysID 10da7d0a16 feat: migrate simple theme switcher component 2025-04-27 05:56:52 +07:00
ArraysID c07c75f23b feat: migrate footer component 2025-04-27 05:51:58 +07:00
ArraysID e9622234fe feat: migrate call to action component 2025-04-27 05:50:47 +07:00
ArraysID aedc2b5dfb feat: migrate testimonials component 2025-04-27 05:49:50 +07:00
ArraysID 5b1b700ccb feat: migrate learning resources component 2025-04-27 05:47:43 +07:00
ArraysID 0478d12ffc feat: migrate community component 2025-04-27 05:44:11 +07:00
ArraysID 5929c5a667 feat: migrate features component 2025-04-27 05:38:34 +07:00
ArraysID 206a426120 feat: migrate hero component 2025-04-27 05:35:49 +07:00
ArraysID b4708ab2aa feat: migrate header component 2025-04-27 05:28:37 +07:00
ArraysID 53b069f242 feat: remove unused tw-animate-css import 2025-04-26 22:51:49 +07:00
ArraysID c2a50a86bd feat: add path mapping for shadcn-ui atoms in tsconfig 2025-04-26 22:38:50 +07:00
ArraysID edec847eee feat: update landing app with shadcnI integration 2025-04-26 22:38:28 +07:00
ArraysID 9182440ab3 feat(shadcn-ui): initialize shadcn ui libs 2025-04-26 22:37:29 +07:00
ArraysID 259262a1ef chore: cleanup nextjs project 2025-04-26 21:13:19 +07:00
Maulana Sodiqin b389235971 feat: add landing page 2025-04-26 20:32:58 +07:00
Maulana Sodiqin 86290cc820 fix: unit test 2025-04-25 14:01:29 +07:00
Maulana Sodiqin 41712be56a feat: cleaning 2025-04-25 13:00:50 +07:00
Naufal AzmiandGitHub 240e684b53 Dimentorin auth login responsive and implementation (#23)
* feat: responsive

* fix: success register

* feat: auth login responsive && auth login implementation
2025-04-09 12:59:33 +07:00
Maulana Sodiqin b1a53df4a9 chore: downgrade vite to v5 2025-04-07 20:50:10 +07:00
Maulana Sodiqin 0709c3a2bd chore: add legacy peerdeps 2025-04-07 19:20:32 +07:00
Maulana Sodiqin f2e1cead83 chore: add workflows 2025-04-07 19:17:28 +07:00
Maulana Sodiqin 1b795f2dfc chore: add workflow 2025-04-07 19:15:39 +07:00
Maulana Sodiqin ce115f63e4 feat: add landing page with nuxt 2025-04-07 19:10:29 +07:00
Maulana SodiqinandGitHub c907c696f5 Merge pull request #22 from IMPHNEN/DimentorinAuth
feat: register page and register success page
2025-04-07 17:46:09 +07:00
Maulana SodiqinandGitHub de76020709 Merge pull request #21 from IMPHNEN/feat/backoffice-roles-permissions
Backoffice permissions & roles page
2025-04-07 11:48:12 +07:00
boboiazumi 1e391a2932 feat: register page and register success page 2025-04-07 11:36:44 +07:00
Hafid Nur a7c67cb77f feat: add permissions and roles page
- Tambah halaman permissions & roles beserta modal
- Update Backoffice Sidebar
- Update styling label pada input field
2025-04-07 02:19:07 +07:00
Maulana SodiqinandGitHub dafdfba490 Merge pull request #19 from IMPHNEN/fix/split-register-form-modal
Fix/split register form modal
2025-04-05 12:36:10 +07:00
Maulana SodiqinandGitHub a741fbb8d4 Merge pull request #20 from IMPHNEN/feat/backoffice-gacha-roll
Backoffice Gacha Roll page
2025-04-05 12:35:28 +07:00
Hafid Nur 0e3dc101d5 feat: update Dashboard page
- Perubahan tampilan dan modal pada halaman Dashboard
- Tambah schema dan type untuk Gacha Item
- Gunakan form dan Toast
2025-04-05 09:36:49 +07:00
Hafid Nur e805e5fd41 fix: update Toast success/error message 2025-04-05 08:48:53 +07:00
Hafid Nur 5bdf8f5a4d feat(backoffice): add Toast 2025-04-05 08:40:19 +07:00
Hafid Nur 07f1fb0208 feat: modal for update gacha roll item
- Sesuaikan modal untuk update gacha roll item
- Refactor custom hook untuk tambah dan update gacha roll item
2025-04-05 08:02:13 +07:00
Hafid Nur 3958ae1098 refactor: fix validation for add gacha roll item
- Pisahkan type dan schema untuk gacha roll item
- Perbaiki validasi gacha roll item
- Parsing angka pada component ControlledInputField
- Update type number untuk component Input dan InputField
2025-04-05 07:39:57 +07:00
Hafid Nur 885d156833 feat: custom hook for add gacha roll item
- Buat custom hook untuk add item
2025-04-05 06:41:11 +07:00
Hafid Nur 2746af0b3f fix: unit test for button and input-field component 2025-04-05 06:21:56 +07:00
Hafid Nur 745a2d2873 feat: slicing Gacha Roll page 2025-04-05 06:07:26 +07:00
Hafid Nur 8887e3b0a9 feat: update ui component sidebar, datatable, button
- Tambah halaman "Gacha Roll" pada sidebar
- Edit style button variant danger
- Adjust sedikit header datatable
2025-04-05 06:05:50 +07:00
89 changed files with 17008 additions and 4567 deletions
+28
View File
@@ -0,0 +1,28 @@
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: Nx Build
run: npx nx run-many --target=build --all
+11
View File
@@ -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
+2 -1
View File
@@ -2,6 +2,7 @@
"recommendations": [
"nrwl.angular-console",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint"
"dbaeumer.vscode-eslint",
"ms-playwright.playwright"
]
}
+3 -6
View File
@@ -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,
};
};
+5 -27
View File
@@ -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;
+210
View File
@@ -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,
};
};
+18
View File
@@ -0,0 +1,18 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
export const AppLayout: FC = (): ReactElement => {
return (
<div className="bg-primary-50 min-h-screen flex justify-center">
<div className="bg-primary-50 min-h-screen w-full flex">
<BackofficeSidebar />
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
<Outlet />
</div>
</div>
</div>
);
};
export default AppLayout;
+203
View File
@@ -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;
+2
View File
@@ -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,
};
};
+41 -30
View File
@@ -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>
+89 -7
View File
@@ -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;
+2
View File
@@ -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>
+30
View File
@@ -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$"
]
}
+21
View File
@@ -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/**/*'],
},
];
+6
View File
@@ -0,0 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
declare module '*.svg' {
const content: any;
export const ReactComponent: any;
export default content;
}
+5
View File
@@ -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.
+22
View File
@@ -0,0 +1,22 @@
//@ts-check
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { composePlugins, withNx } = require('@nx/next');
/**
* @type {import('@nx/next/plugins/with-nx').WithNxOptions}
**/
const nextConfig = {
nx: {
// Set this to true if you would like to to use SVGR
// See: https://github.com/gregberge/svgr
svgr: false,
},
};
const plugins = [
// Add more Next.js plugins to this list if needed.
withNx,
];
module.exports = composePlugins(...plugins)(nextConfig);
+7
View File
@@ -0,0 +1,7 @@
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
};
export default config;
+24
View File
@@ -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"
}
}
}
}
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

@@ -0,0 +1,66 @@
'use client';
import { Button } from '@imphnen-frontend-service/shadcn-ui/atoms';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
export function CallToAction() {
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">
Siap Menjadi{' '}
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
Programmer Handal?
</span>
</h2>
<p className="text-lg text-muted-foreground mb-8 max-w-2xl mx-auto">
Bergabunglah dengan komunitas IMPHNEN sekarang dan mulai
perjalanan programming mu dengan cara yang menyenangkan!
</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"
>
Gabung Discord
</Button>
<Button
size="lg"
variant="outline"
className="border-primary hover:bg-primary/10"
>
Join Facebook Group
</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,179 @@
'use client';
import {
Button,
FacebookIcon,
InstagramIcon,
MessageCircleIcon,
} from '@imphnen-frontend-service/shadcn-ui/atoms';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
export function Community() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, amount: 0.2 });
const communities = [
{
icon: (
<FacebookIcon className="h-6 w-6 text-blue-600 dark:text-blue-400" />
),
iconBg: 'bg-blue-100 dark:bg-blue-900',
title: 'Facebook Group',
description:
'Bergabunglah dengan grup Facebook kami untuk diskusi santai dan berbagi artikel menarik.',
buttonText: 'Gabung Sekarang',
buttonLink: '#',
},
{
icon: (
<InstagramIcon className="h-6 w-6 text-blue-600 dark:text-blue-400" />
),
iconBg: 'bg-blue-100 dark:bg-blue-900',
title: 'Instagram',
description:
'Ikuti kami di Instagram untuk tips programming, konten inspiratif, dan info event terbaru.',
buttonText: 'Follow Kami',
buttonLink: '#',
},
{
icon: (
<MessageCircleIcon className="h-6 w-6 text-blue-600 dark:text-blue-400" />
),
iconBg: 'bg-blue-100 dark:bg-blue-900',
title: 'Discord Server',
description:
'Diskusikan langsung dengan sesama programmer dan dapatkan bantuan langsung dari para ahli.',
buttonText: 'Join Server',
buttonLink: '#',
},
];
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"
>
{/* Background Elements */}
<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}>
<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 }}
>
<h2 className="text-3xl font-bold tracking-tighter md:text-4xl/tight lg:text-5xl">
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 md:text-lg">
Bergabunglah dengan ribuan programmer Indonesia yang saling
membantu dan berbagi pengalaman.
</p>
</motion.div>
</div>
<motion.div
className="grid gap-8 md:grid-cols-3"
variants={containerVariants}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
>
{communities.map((community, index) => (
<motion.div
key={index}
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 ${community.iconBg}`}
>
{community.icon}
</div>
<h3 className="mb-2 text-xl font-bold">{community.title}</h3>
<p className="mb-6 text-muted-foreground">
{community.description}
</p>
<Button
variant="outline"
className="w-full group-hover:bg-primary group-hover:text-primary-foreground transition-colors duration-300"
>
{community.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>
{/* Community Stats */}
<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 } : { opacity: 0, y: 20 }}
transition={{ duration: 0.5, delay: 0.3 }}
>
<div className="space-y-2">
<div className="text-4xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
10K+
</div>
<div className="text-sm text-muted-foreground">Member Aktif</div>
</div>
<div className="space-y-2">
<div className="text-4xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
50+
</div>
<div className="text-sm text-muted-foreground">Event Bulanan</div>
</div>
<div className="space-y-2">
<div className="text-4xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
100+
</div>
<div className="text-sm text-muted-foreground">
Mentor Profesional
</div>
</div>
<div className="space-y-2">
<div className="text-4xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
5K+
</div>
<div className="text-sm text-muted-foreground">
Diskusi Mingguan
</div>
</div>
</motion.div>
</div>
</section>
);
}
@@ -0,0 +1,126 @@
'use client';
import {
BookOpenIcon,
CodeIcon,
LaptopIcon,
UsersIcon,
} from '@imphnen-frontend-service/shadcn-ui/atoms';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
export function Features() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, amount: 0.2 });
const features = [
{
icon: <LaptopIcon className="h-6 w-6 text-primary" />,
title: 'Belajar Tanpa Koding',
description:
'Pelajari konsep programming dengan cara yang mudah dipahami tanpa harus menulis kode yang rumit.',
},
{
icon: <UsersIcon className="h-6 w-6 text-primary" />,
title: 'Komunitas Supportif',
description:
'Bergabunglah dengan komunitas programmer Indonesia yang siap membantu dan berbagi pengalaman.',
},
{
icon: <BookOpenIcon className="h-6 w-6 text-primary" />,
title: 'Tutorial Interaktif',
description:
'Akses tutorial interaktif yang membuat konsep programming lebih mudah untuk dipahami.',
},
{
icon: <CodeIcon className="h-6 w-6 text-primary" />,
title: 'Proyek Praktis',
description:
'Terapkan pengetahuan Anda dalam proyek nyata dengan panduan langkah demi langkah.',
},
];
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"
>
{/* Background Elements */}
<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">
Belajar programming dengan{' '}
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
cara yang lebih baik
</span>
</h2>
<p className="max-w-[800px] mx-auto text-muted-foreground md:text-lg">
IMPHNEN hadir dengan berbagai fitur untuk membantu kamu menjadi
programmer handal tanpa harus pusing dengan coding.
</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">
{feature.icon}
</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>
);
}
+207
View File
@@ -0,0 +1,207 @@
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 font-medium text-primary-foreground shadow hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50">
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>
);
}
+145
View File
@@ -0,0 +1,145 @@
'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">
<Image
src="/logo.png"
alt="IMPHNEN Logo"
width={64}
height={64}
className="object-cover"
/>
</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">
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>
<Button className="mt-4 bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90">
Gabung Discord
</Button>
</nav>
</div>
)}
</header>
);
}
+153
View File
@@ -0,0 +1,153 @@
'use client';
import {
Button,
CodeIcon,
SparklesIcon,
UsersIcon,
} from '@imphnen-frontend-service/shadcn-ui/atoms';
import { motion } from 'framer-motion';
import Image from 'next/image';
import { useEffect, useState } from 'react';
export function Hero() {
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">
{/* Background Elements */}
<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" />
{/* Animated Gradient Orbs */}
<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),
}}
/>
{/* Grid Pattern */}
<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">
<SparklesIcon className="mr-1 h-3.5 w-3.5 text-primary" />
<span>Komunitas Programmer Indonesia</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">
Programmer Handal, <br />
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
Tanpa Ribet
</span>
</h1>
<p className="max-w-[600px] text-muted-foreground md:text-xl">
Temukan potensi programming Anda bersama komunitas yang
mendukung, tutorial interaktif, dan sumber daya berkualitas
tinggi.
</p>
</div>
<div className="flex flex-col sm:flex-row gap-4">
<a href="https://facebook.com">
<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"
>
Mulai Belajar
</Button>
</a>
<Button
size="lg"
variant="outline"
className="group relative overflow-hidden border-primary"
>
<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>
<span className="relative">Gabung Discord</span>
</Button>
</div>
<div className="flex items-center gap-8">
<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">
100K+
</div>
<div className="text-xs text-muted-foreground">Member</div>
</div>
<div className="h-10 border-r border-border"></div>
<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">
500+
</div>
<div className="text-xs text-muted-foreground">Tutorial</div>
</div>
<div className="h-10 border-r border-border"></div>
<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">
24/7
</div>
<div className="text-xs text-muted-foreground">Yapping</div>
</div>
</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">
{/* Decorative Elements */}
<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>
{/* Main Image */}
<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" />
<Image
src="/hero.jpeg"
width={600}
height={500}
alt="IMPHNEN Programming Community"
className="w-full h-auto object-cover"
/>
</div>
</div>
</motion.div>
</div>
</div>
</section>
);
}
@@ -0,0 +1,192 @@
'use client';
import {
BookOpenIcon,
Button,
CodeIcon,
Share2Icon,
VideoIcon,
} from '@imphnen-frontend-service/shadcn-ui/atoms';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
export function LearningResources() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, amount: 0.2 });
const resources = [
{
icon: <VideoIcon className="h-6 w-6 text-blue-600 dark:text-blue-400" />,
iconBg: 'bg-blue-100 dark:bg-blue-900',
title: 'Video Tutorial',
description:
'Belajar melalui tutorial video dari langkah awal hingga mahir.',
buttonText: 'Lihat Semua Video',
buttonLink: '#',
color: 'from-blue-500 to-blue-700',
},
{
icon: (
<BookOpenIcon className="h-6 w-6 text-blue-600 dark:text-blue-400" />
),
iconBg: 'bg-blue-100 dark:bg-blue-900',
title: 'Artikel & Tutorial',
description:
'Pelajari konsep programming melalui artikel yang disusun secara terstruktur.',
buttonText: 'Baca Artikel',
buttonLink: '#',
color: 'from-blue-500 to-blue-700',
},
{
icon: <CodeIcon className="h-6 w-6 text-blue-600 dark:text-blue-400" />,
iconBg: 'bg-blue-100 dark:bg-blue-900',
title: 'Tantangan Koding',
description:
'Uji kemampuan koding kamu dengan tantangan yang menyenangkan dan menantang.',
buttonText: 'Mulai Tantangan',
buttonLink: '#',
color: 'from-blue-500 to-blue-700',
},
{
icon: <Share2Icon className="h-6 w-6 text-blue-600 dark:text-blue-400" />,
iconBg: 'bg-blue-100 dark:bg-blue-900',
title: 'Sharing Session',
description:
'Ikuti sesi berbagi pengalaman dari programmer berpengalaman dan belajar dari pengalaman mereka.',
buttonText: 'Jadwal Session',
buttonLink: '#',
color: 'from-blue-500 to-blue-700',
},
];
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"
>
{/* Background Elements */}
<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
className="space-y-2"
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
transition={{ duration: 0.5 }}
>
<h2 className="text-3xl font-bold tracking-tighter md:text-4xl/tight lg:text-5xl">
Sumber{' '}
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
Belajar
</span>
</h2>
<p className="max-w-[800px] mx-auto text-muted-foreground md:text-lg">
Akses berbagai materi belajar yang akan membantu kamu menguasai
konsep programming dengan cara yang menyenangkan.
</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((resource, index) => (
<motion.div
key={index}
className="group relative overflow-hidden rounded-xl border bg-background p-6 transition-all 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 ${resource.iconBg}`}
>
{resource.icon}
</div>
<h3 className="mb-2 text-xl font-bold">{resource.title}</h3>
<p className="mb-6 text-muted-foreground">
{resource.description}
</p>
<Button
variant="link"
className="p-0 h-auto font-medium text-primary hover:text-primary/80"
>
{resource.buttonText}
</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>
{/* Featured Resource */}
<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 } : { opacity: 0, y: 20 }}
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">
Rekomendasi Terbaik
</div>
<h3 className="text-2xl md:text-3xl font-bold mb-4">
Kursus Lengkap Web Development
</h3>
<p className="text-muted-foreground mb-6">
Pelajari HTML, CSS, JavaScript, React, dan Node.js dalam satu
kursus komprehensif yang dirancang untuk pemula hingga tingkat
menengah.
</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">
Mulai Kursus
</Button>
<Button variant="outline">Lihat Silabus</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"
>
{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,190 @@
'use client';
import { QuoteIcon } from '@imphnen-frontend-service/shadcn-ui/atoms';
import { motion, useInView } from 'framer-motion';
import Image from 'next/image';
import { useRef } from 'react';
export function Testimonials() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, amount: 0.2 });
const testimonials = [
{
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: '/placeholder.svg?height=64&width=64',
},
{
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: '/placeholder.svg?height=64&width=64',
},
{
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: '/placeholder.svg?height=64&width=64',
},
];
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"
>
{/* Background Elements */}
<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" 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 }}
>
<h2 className="text-3xl font-bold tracking-tighter md:text-4xl/tight lg:text-5xl">
Testimoni{' '}
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
Member
</span>
</h2>
<p className="max-w-[800px] mx-auto text-muted-foreground md:text-lg">
Apa kata mereka yang telah bergabung dengan komunitas IMPHNEN?
</p>
</motion.div>
</div>
<motion.div
className="grid gap-8 md:grid-cols-3"
variants={containerVariants}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
>
{testimonials.map((testimonial, index) => (
<motion.div
key={index}
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">
"{testimonial.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">
<Image
src={testimonial.avatar || '/placeholder.svg'}
alt={testimonial.name}
width={48}
height={48}
className="object-cover"
/>
</div>
<div>
<h4 className="font-bold">{testimonial.name}</h4>
<p className="text-sm text-muted-foreground">
{testimonial.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>
{/* Testimonial Stats */}
<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 } : { opacity: 0, y: 20 }}
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">
Bergabunglah dengan{' '}
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
10,000+
</span>{' '}
programmer Indonesia lainnya
</h3>
<p className="text-muted-foreground">
Komunitas kami terus berkembang dengan programmer dari berbagai
latar belakang dan tingkat keahlian. Bersama-sama, kita belajar,
berbagi, dan tumbuh sebagai profesional.
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div 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">
98%
</div>
<div className="text-sm text-muted-foreground">
Tingkat Kemalasan
</div>
</div>
<div 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">
4.9/5
</div>
<div className="text-sm text-muted-foreground">
Rating Drama
</div>
</div>
<div 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">
85%
</div>
<div className="text-sm text-muted-foreground">
Mendapat Pekerjaan
</div>
</div>
<div 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">
24/7
</div>
<div className="text-sm text-muted-foreground">Yapping</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>;
}
+32
View File
@@ -0,0 +1,32 @@
import { type Metadata } from 'next';
import { Inter } from 'next/font/google';
import '../styles/globals.css';
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
>
{children}
</ThemeProvider>
</body>
</html>
);
}
+25
View File
@@ -0,0 +1,25 @@
import { CallToAction } from './_components/call-to-action';
import { Community } from './_components/community';
import { Features } from './_components/features';
import Footer from './_components/footer';
import { Header } from './_components/header';
import { Hero } from './_components/hero';
import { LearningResources } from './_components/learning-resources';
import { Testimonials } from './_components/testimonials';
export default function Page() {
return (
<div className="flex min-h-screen flex-col">
<Header />
<main className="flex-1">
<Hero />
<Features />
<Community />
<LearningResources />
<Testimonials />
<CallToAction />
</main>
<Footer />
</div>
);
}
+3
View File
@@ -0,0 +1,3 @@
@import 'tailwindcss';
@import "../../../../libs/shadcn-ui/src/index.css";
@source "../../../../libs/shadcn-ui/src/atoms/**/*.{ts,tsx}";
+44
View File
@@ -0,0 +1,44 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "preserve",
"strict": true,
"noEmit": true,
"emitDeclarationOnly": false,
"esModuleInterop": true,
"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
View File
@@ -12,7 +12,7 @@ export default [
],
},
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'],
rules: {
'@nx/enforce-module-boundaries': [
'error',
+49
View File
@@ -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
View File
@@ -1 +1,2 @@
export * from './auth';
export * from './gacha';
+11 -1
View File
@@ -1 +1,11 @@
export {};
export type TGachaItem = {
itemName: string;
quantity: number;
foto?: File;
};
export type TGachaRollItem = {
itemName: string;
quantity: number;
chanceRate: number;
};
+12
View File
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
+7
View File
@@ -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/).
+21
View File
@@ -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"
}
+1654
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -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"
}
}
+9
View File
@@ -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": {}
}
+58
View File
@@ -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 };
+37
View File
@@ -0,0 +1,37 @@
import {
LuArrowDown as ArrowDownIcon,
LuBookOpen as BookOpenIcon,
LuCode as CodeIcon,
LuFacebook as FacebookIcon,
LuInstagram as InstagramIcon,
LuLaptop as LaptopIcon,
LuMenu as MenuIcon,
LuMessageCircle as MessageCircleIcon,
LuMoon as MoonIcon,
LuQuote as QuoteIcon,
LuShare2 as Share2Icon,
LuSparkles as SparklesIcon,
LuSun as SunIcon,
LuUsers as UsersIcon,
LuVideo as VideoIcon,
LuX as XIcon,
} from 'react-icons/lu';
export {
ArrowDownIcon,
BookOpenIcon,
CodeIcon,
FacebookIcon,
InstagramIcon,
LaptopIcon,
MenuIcon,
MessageCircleIcon,
MoonIcon,
QuoteIcon,
Share2Icon,
SparklesIcon,
SunIcon,
UsersIcon,
VideoIcon,
XIcon,
};
+2
View File
@@ -0,0 +1,2 @@
export * from './button';
export * from './icons';
+134
View File
@@ -0,0 +1,134 @@
@import 'tailwindcss';
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 1rem;
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.623 0.214 259.815);
--primary-foreground: oklch(0.97 0.014 254.604);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.623 0.214 259.815);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.623 0.214 259.815);
--sidebar-primary-foreground: oklch(0.97 0.014 254.604);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.623 0.214 259.815);
}
.dark {
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.546 0.245 262.881);
--primary-foreground: oklch(0.379 0.146 265.522);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.488 0.243 264.376);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.546 0.245 262.881);
--sidebar-primary-foreground: oklch(0.379 0.146 265.522);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.488 0.243 264.376);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
/* https://github.com/tailwindlabs/tailwindcss/issues/13129 */
.container {
margin-left: auto;
margin-right: auto;
padding-left: 2rem;
padding-right: 2rem;
width: 100%;
}
@media (min-width: 1536px) {
.container {
max-width: 1400px;
}
}
+1
View File
@@ -0,0 +1 @@
export * from './atoms/index';
+18
View File
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"types": ["vite/client"]
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
],
"extends": "../../tsconfig.base.json"
}
+25
View File
@@ -0,0 +1,25 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts",
"vite/client",
"next",
"@nx/next/typings/image.d.ts"
]
},
"exclude": [
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx"
],
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
}
+48
View File
@@ -0,0 +1,48 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import dts from 'vite-plugin-dts';
import * as path from 'path';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../node_modules/.vite/libs/shadcn-ui',
plugins: [
react(),
nxViteTsPaths(),
nxCopyAssetsPlugin(['*.md']),
dts({
entryRoot: 'src',
tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'),
}),
],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: '../../dist/libs/shadcn-ui',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
name: 'shadcn-ui',
fileName: 'index',
// Change this to the formats you want to support.
// Don't forget to update your package.json as well.
formats: ['es' as const],
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: ['react', 'react-dom', 'react/jsx-runtime'],
},
},
}));
+3 -3
View File
@@ -24,9 +24,9 @@ describe('Test Button Component', () => {
const button = screen.getByText('Delete');
expect(button).toHaveClass('bg-danger-500');
expect(button).toHaveClass('hover:bg-danger-600');
expect(button).toHaveClass('text-white');
expect(button).toHaveClass('bg-danger-100');
expect(button).toHaveClass('hover:bg-danger-200');
expect(button).toHaveClass('text-danger-500');
});
it("disables the button when 'disabled' prop is set", async () => {
+1 -1
View File
@@ -31,7 +31,7 @@ const variantClasses: Record<TButtonVariant, string> = {
bordered:
'border border-primary-500 hover:border-primary-600 bg-transparent hover:text-primary-600 hover:bg-gray-50 text-primary-500',
success: 'bg-success-500 hover:bg-success-600 text-white shadow-md',
danger: 'bg-danger-500 hover:bg-danger-600 text-white shadow-md',
danger: 'bg-danger-100 hover:bg-danger-200 text-danger-500 shadow-md',
};
const sizeClasses: Record<TButtonSize, string> = {
+5 -2
View File
@@ -9,8 +9,9 @@ import { EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons'; // Import
import { cn } from '@imphnen-frontend-service/utils';
import { Button } from '../button';
type TInputType = 'text' | 'email' | 'password' | 'file';
type TInputType = 'text' | 'email' | 'number' | 'password' | 'file';
type TInputSize = 'sm' | 'md' | 'lg';
type Width = 'standard' | 'custom'
type TInputProps = Omit<
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
@@ -18,6 +19,7 @@ type TInputProps = Omit<
> & {
type?: TInputType;
size?: TInputSize;
widthform?: Width;
disabled?: boolean;
};
@@ -34,6 +36,7 @@ export const Input: FC<TInputProps> = ({
type = 'text',
size = 'md',
placeholder = 'Placeholder',
widthform='standard',
disabled,
className,
...rest
@@ -46,7 +49,7 @@ export const Input: FC<TInputProps> = ({
};
const mergedClassName = cn(
'px-[12px] py-[8px] text-neutral-800 bg-white placeholder:text-neutral-300 border border-neutral-200 hover:border-blue-300 focus:outline-1 focus:outline-blue-500 rounded-md font-bai-jamjuree min-w-70',
`px-[12px] py-[8px] text-neutral-800 bg-white placeholder:text-neutral-300 border border-neutral-200 hover:border-blue-300 focus:outline-1 focus:outline-blue-500 rounded-md font-bai-jamjuree ${widthform === "standard" ? "min-w-70" : ""}`,
sizeClasses[size].textSize,
disabled && disabledClass,
className
@@ -3,7 +3,7 @@ import { InputField } from './input-field';
describe('InputField Component', () => {
it('renders correctly with disabled prop', () => {
render(<InputField label="Test Label" disabled={true} />);
render(<InputField htmlFor="test" label="Test Label" disabled={true} />);
const input = screen.getByLabelText('Test Label');
expect(input).toBeDisabled();
@@ -11,7 +11,7 @@ describe('InputField Component', () => {
});
it('renders correctly without disabled prop', () => {
render(<InputField label="Test Label" disabled={false} />);
render(<InputField htmlFor="test" label="Test Label" disabled={false} />);
const input = screen.getByLabelText('Test Label');
expect(input).not.toBeDisabled();
@@ -7,7 +7,7 @@ import {
import { Input } from '../../atoms';
import { cn } from '@imphnen-frontend-service/utils';
export type TInputType = 'text' | 'email' | 'password' | 'file';
export type TInputType = 'text' | 'email' | 'number' | 'password' | 'file';
export type TInputSize = 'sm' | 'md' | 'lg';
export type TInputFieldProps = Omit<
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
@@ -54,7 +54,7 @@ export const InputField: FC<TInputFieldProps> = ({
<label
htmlFor={htmlFor}
className={cn(
'items-start justify-item-start text-start',
'items-start justify-item-start text-start !text-neutral-800',
sizeClasses[size].label
)}
>
@@ -70,7 +70,7 @@ export const InputField: FC<TInputFieldProps> = ({
error &&
'border-danger-500 hover:border-danger-500 focus:outline-danger-500',
className,
disabled && 'opacity-50 cursor-not-allowed' // Add styles for disabled state
disabled && 'opacity-50 cursor-not-allowed'
)}
{...rest}
/>
@@ -5,7 +5,7 @@ import { FC, ReactElement } from "react";
function AuthBanner({text, href}: {text: string, href: string}){
return (
<div className='relative rounded-md'>
<div className='hidden xl:block relative rounded-md'>
<img src="/image/95319c4f9953dfe6180200e529dfcea5.webp" alt="Banner" className='w-[420px] h-[632px] object-[80%] object-cover rounded-lg' />
<div className='absolute top-0 bg-gradient-to-b from-primary-500 to-transparent w-full rounded-t-lg h-[163px] py-5 px-5'>
<Button variant='secondary' className='gap-2' onClick={() => document.location.href = `${href}`}>
@@ -3,7 +3,10 @@ import {
AuditOutlined,
InboxOutlined,
LogoutOutlined,
ReloadOutlined,
UsergroupAddOutlined,
UserOutlined,
UserSwitchOutlined,
} from '@ant-design/icons';
import { Button } from '../../atoms';
import { FC, ReactElement } from 'react';
@@ -33,6 +36,42 @@ export const BackofficeSidebar: FC = (): ReactElement => {
<span className="text-p3 font-medium">Dashboard & Set Gacha</span>
</Link>
<Link
to="/gacha-roll"
className={`flex items-center justify-items-start gap-3 px-[8px] py-[10px] ${
isActive('/gacha-roll')
? 'bg-primary-500 text-white rounded-md'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
<ReloadOutlined className="text-[20px]" />
<span className="text-p3 font-medium">Gacha Roll</span>
</Link>
<Link
to="/permissions"
className={`flex items-center justify-items-start gap-3 px-[8px] py-[10px] ${
isActive('/permissions')
? 'bg-primary-500 text-white rounded-md'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
<UserSwitchOutlined className="text-[20px]" />
<span className="text-p3 font-medium">Permissions</span>
</Link>
<Link
to="/roles"
className={`flex items-center justify-items-start gap-3 px-[8px] py-[10px] ${
isActive('/roles')
? 'bg-primary-500 text-white rounded-md'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
<UsergroupAddOutlined className="text-[20px]" />
<span className="text-p3 font-medium">Roles</span>
</Link>
<Link
to="/accounts"
className={`flex items-center justify-items-start gap-3 px-[8px] py-[10px] ${
@@ -15,7 +15,30 @@ export const ControlledInputField = <T extends FieldValues>(
props: TControlledInputFieldProps<T>
) => {
const { field, fieldState } = useController<T>(props);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
let value;
if (props.type === 'number') {
value = Number(e.target.value);
} else if (props.type === 'file') {
value = e.target.files?.[0];
} else {
value = e.target.value;
}
field.onChange(value);
};
const inputProps =
props.type === 'file'
? { ...props, ...field, value: undefined }
: { ...props, ...field };
return (
<InputField error={fieldState.error?.message} {...{ ...props, ...field }} />
<InputField
error={fieldState.error?.message}
{...inputProps}
onChange={handleChange}
/>
);
};
@@ -43,7 +43,7 @@ export const DataTable = <T,>({
<div className="flex flex-col gap-8">
<div className="w-full overflow-x-auto">
<table className="w-full min-w-full text-base">
<thead className="bg-primary-50 mb-3 text-left">
<thead className="bg-primary-50 mb-3 text-left text-nowrap">
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
+27 -67
View File
@@ -7,6 +7,7 @@ import {
} from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { Navbar } from './navbar';
import { ModalLoginProvider } from '@imphnen-frontend-service/utils';
interface MatchMediaResult {
matches: boolean;
@@ -37,7 +38,16 @@ function mockMatchMedia(width: { matches: boolean }): void {
});
}
describe('Navbar', () => {
const renderWithProvider = () =>
render(
<BrowserRouter>
<ModalLoginProvider>
<Navbar />
</ModalLoginProvider>
</BrowserRouter>
);
describe('Navbar Component', () => {
beforeEach(() => {
document.body.innerHTML = '';
});
@@ -46,95 +56,57 @@ describe('Navbar', () => {
vi.restoreAllMocks();
});
it('renders the logo', () => {
render(
<BrowserRouter>
<Navbar />
</BrowserRouter>
);
it('Test renders the logo', () => {
renderWithProvider();
const logo: HTMLElement = screen.getByAltText(/IMPHNEN Logo/i);
expect(logo).toBeInTheDocument();
});
it('renders the Home link', () => {
render(
<BrowserRouter>
<Navbar />
</BrowserRouter>
);
it('Test renders the Home link', () => {
renderWithProvider();
const homeLink: HTMLElement = screen.getByRole('link', { name: /home/i });
expect(homeLink).toBeInTheDocument();
});
it('renders the Merch Gacha link', () => {
render(
<BrowserRouter>
<Navbar />
</BrowserRouter>
);
it('Test renders the Merch Gacha link', () => {
renderWithProvider();
const merchLink: HTMLElement = screen.getByRole('link', {
name: /merch gacha/i,
});
expect(merchLink).toBeInTheDocument();
});
it('displays horizontal menu on desktop screens', () => {
it('Test displays horizontal menu on desktop screens', () => {
mockMatchMedia({ matches: true });
global.innerWidth = 1024;
global.dispatchEvent(new Event('resize'));
render(
<BrowserRouter>
<Navbar />
</BrowserRouter>
);
renderWithProvider();
const navItems: HTMLElement = screen.getByRole('list');
expect(navItems).toHaveClass('md:flex');
const hamburgerButton: HTMLElement = screen.getByRole('button');
expect(hamburgerButton).toHaveClass('md:hidden');
});
it('displays hamburger menu on mobile screens and can toggle dropdown', () => {
it('Test displays hamburger menu on mobile screens and can toggle dropdown', () => {
mockMatchMedia({ matches: false });
global.innerWidth = 375;
global.dispatchEvent(new Event('resize'));
render(
<BrowserRouter>
<Navbar />
</BrowserRouter>
);
renderWithProvider();
const hamburgerButton: HTMLElement = screen.getByRole('button');
expect(hamburgerButton).toBeVisible();
fireEvent.click(hamburgerButton);
const dropdownMenu: HTMLElement[] = screen.getAllByRole('list');
expect(dropdownMenu[1]).toBeVisible();
fireEvent.click(hamburgerButton);
const updatedDropdownMenus: HTMLElement[] = screen.getAllByRole('list');
expect(updatedDropdownMenus.length).toBe(1);
});
it('has correct height on tablet screens', () => {
it('Test has correct height on tablet screens', () => {
mockMatchMedia({ matches: true });
global.innerWidth = 768;
global.dispatchEvent(new Event('resize'));
const { container }: RenderResult = render(
<BrowserRouter>
<Navbar />
</BrowserRouter>
);
const { container }: RenderResult = renderWithProvider();
const header: HTMLElement | null = container.querySelector('header');
expect(header).not.toBeNull();
if (header) {
@@ -143,18 +115,11 @@ describe('Navbar', () => {
}
});
it('has max width constraint on large screens', () => {
it('Test has max width constraint on large screens', () => {
mockMatchMedia({ matches: true });
global.innerWidth = 3840;
global.dispatchEvent(new Event('resize'));
const { container }: RenderResult = render(
<BrowserRouter>
<Navbar />
</BrowserRouter>
);
const { container }: RenderResult = renderWithProvider();
const header: HTMLElement | null = container.querySelector('header');
expect(header).not.toBeNull();
if (header) {
@@ -163,13 +128,8 @@ describe('Navbar', () => {
}
});
it('has correct ARIA role for nav', () => {
const { container }: RenderResult = render(
<BrowserRouter>
<Navbar />
</BrowserRouter>
);
it('Test has correct ARIA role for nav', () => {
const { container }: RenderResult = renderWithProvider();
const header: HTMLElement | null = container.querySelector('header');
expect(header).toHaveAttribute('role', 'nav');
});
+28
View File
@@ -45,6 +45,23 @@
"testStorybookTargetName": "test-storybook",
"staticStorybookTargetName": "static-storybook"
}
},
{
"plugin": "@nx/next/plugin",
"options": {
"startTargetName": "start",
"buildTargetName": "build",
"devTargetName": "dev",
"serveStaticTargetName": "serve-static",
"buildDepsTargetName": "build-deps",
"watchDepsTargetName": "watch-deps"
}
},
{
"plugin": "@nx/playwright/plugin",
"options": {
"targetName": "e2e"
}
}
],
"generators": {
@@ -63,11 +80,22 @@
"linter": "eslint",
"unitTestRunner": "vitest"
}
},
"@nx/next": {
"application": {
"style": "none",
"linter": "eslint"
}
}
},
"release": {
"version": {
"preVersionCommand": "npx nx run-many -t build"
}
},
"targetDefaults": {
"e2e-ci--**/**": {
"dependsOn": ["^build"]
}
}
}
+10761 -4232
View File
File diff suppressed because it is too large Load Diff
+43 -20
View File
@@ -5,30 +5,42 @@
"scripts": {
"gacha:dev": "nx serve gacha",
"gacha:build": "nx build gacha",
"gacha:test": "nx test gacha",
"gacha:prod": "serve ./dist/apps/gacha",
"backoffice:dev": "nx serve backoffice",
"backoffice:build": "nx build backoffice",
"backoffice:test": "nx test backoffice",
"backoffice:prod": "serve ./dist/apps/backoffice",
"dimentorin:dev": "nx serve dimentorin",
"dimentorin:test": "nx test dimentorin",
"dimentorin:build": "nx build dimentorin",
"dimentorin:prod": "serve ./dist/apps/dimentorin",
"landing:dev": "nx serve landing",
"landing:build": "nx build landing",
"landing:prod": "cd ./dist/apps/landing && npx next start",
"ui:test": "nx test ui",
"ui:build": "nx build ui",
"ui:storybook": "nx storybook ui"
},
"private": true,
"dependencies": {
"@ant-design/icons": "^5.6.1",
"@ant-design/icons": "^6.0.0",
"@hookform/resolvers": "^5.0.1",
"@tanstack/react-query": "^5.67.3",
"@radix-ui/react-slot": "^1.2.0",
"@tanstack/react-query": "^5.74.4",
"@tanstack/react-store": "^0.7.0",
"@tanstack/react-table": "^8.21.2",
"axios": "^1.8.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.9.2",
"js-cookie": "^3.0.5",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next": "~15.2.4",
"next-themes": "^0.4.6",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-hook-form": "^7.55.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.3.0",
"sonner": "^2.0.3",
"tailwind-merge": "^3.0.2",
@@ -37,15 +49,22 @@
"devDependencies": {
"@babel/core": "^7.14.5",
"@babel/preset-react": "^7.14.5",
"@eslint/compat": "^1.1.1",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.8.0",
"@nx/eslint": "20.5.0",
"@nx/eslint-plugin": "20.5.0",
"@nx/js": "20.6.0",
"@nx/react": "20.5.0",
"@nx/storybook": "^20.6.0",
"@nx/vite": "20.5.0",
"@nx/web": "20.6.0",
"@nx/workspace": "20.5.0",
"@next/eslint-plugin-next": "^15.2.4",
"@nx/devkit": "20.8.1",
"@nx/eslint": "20.8.1",
"@nx/eslint-plugin": "20.8.1",
"@nx/js": "20.8.1",
"@nx/next": "^20.8.1",
"@nx/playwright": "20.8.1",
"@nx/react": "^20.8.1",
"@nx/storybook": "20.8.1",
"@nx/vite": "20.8.1",
"@nx/web": "20.8.1",
"@nx/workspace": "20.8.1",
"@playwright/test": "^1.36.0",
"@storybook/addon-essentials": "^8.4.6",
"@storybook/addon-interactions": "^8.4.6",
"@storybook/core-server": "^8.4.6",
@@ -54,7 +73,7 @@
"@storybook/test-runner": "^0.19.0",
"@storybook/testing-library": "^0.2.2",
"@swc-node/register": "~1.9.1",
"@swc/cli": "^0.6.0",
"@swc/cli": "0.6.0",
"@swc/core": "~1.5.7",
"@swc/helpers": "~0.5.11",
"@tailwindcss/postcss": "^4.0.13",
@@ -64,32 +83,36 @@
"@testing-library/user-event": "^14.6.1",
"@types/js-cookie": "^3.0.6",
"@types/node": "18.16.9",
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.2.0",
"@vitest/coverage-v8": "^3.0.5",
"@vitest/ui": "^3.0.0",
"ajv": "^6.12.6",
"eslint": "^9.8.0",
"eslint-config-prettier": "^9.0.0",
"eslint-config-next": "^15.2.4",
"eslint-config-prettier": "10.0.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-jsx-a11y": "6.10.1",
"eslint-plugin-playwright": "^1.6.2",
"eslint-plugin-react": "7.35.0",
"eslint-plugin-react-hooks": "5.0.0",
"h3": "^1.8.2",
"jiti": "2.4.2",
"jsdom": "~22.1.0",
"nx": "20.5.0",
"nx": "20.8.1",
"postcss": "^8.5.3",
"prettier": "^2.6.2",
"serve": "^14.2.4",
"storybook": "^8.4.6",
"tailwindcss": "^4.0.13",
"ts-node": "10.9.1",
"ts-node": "^10.9.2",
"tslib": "^2.3.0",
"typescript": "~5.7.2",
"tw-animate-css": "^1.2.8",
"typescript": "^5.8.3",
"typescript-eslint": "^8.19.0",
"verdaccio": "^6.0.5",
"vite": "^6.0.0",
"vite": "^6.3.3",
"vite-plugin-dts": "~4.5.0",
"vitest": "^3.0.8"
},
+1
View File
@@ -16,6 +16,7 @@
"baseUrl": ".",
"paths": {
"@imphnen-frontend-service/service": ["libs/service/src/index.ts"],
"@imphnen-frontend-service/shadcn-ui/atoms": ["libs/shadcn-ui/src/atoms/index.ts"],
"@imphnen-frontend-service/ui/atoms": ["libs/ui/src/atoms/index.ts"],
"@imphnen-frontend-service/ui/molecules": [
"libs/ui/src/molecules/index.ts"