Compare commits

..
Author SHA1 Message Date
ArraysID a84e2fc31a fix: remove mistakenly added env from test-build workflow 2025-05-11 05:53:31 +07:00
ArraysID 6e1c561387 Merge branch 'feat/landing-cms' of https://github.com/IMPHNEN/imphnen-frontend-service into feat/landing-cms 2025-05-11 05:36:24 +07:00
ArraysID b39e918b3c fix: remove unused environment variables from test build workflow 2025-05-11 05:32:55 +07:00
ArraysID 0a217341a5 chore: add environment placeholder variables for CMS configuration in test build workflow 2025-05-11 05:32:55 +07:00
ArraysID f1f72402b2 fix: add placeholder to .env.example 2025-05-11 05:32:55 +07:00
ArraysID 3f1b2a2bd7 fix: supress scss deps warning 2025-05-11 05:32:55 +07:00
ArraysID 57b27c6784 feat: wrap logo image in a link to navigate to the homepage 2025-05-11 05:32:55 +07:00
ArraysID f191ffc4b1 feat: update button components to use onClick for external links and improve styling 2025-05-11 05:32:25 +07:00
ArraysID 31e4ffae92 feat: update CallToActionSection label and wrap Discord button in link 2025-05-11 05:29:34 +07:00
ArraysID c9f7ce24ed feat: implement CallToAction section with dynamic data fetching and UI integration 2025-05-11 05:29:01 +07:00
ArraysID d21adebf73 feat: simplify joinTitle rendering in Testimonials section 2025-05-11 05:28:20 +07:00
ArraysID 744d39ea75 feat: implement Testimonials section with dynamic data fetching and UI integration 2025-05-11 05:28:20 +07:00
ArraysID da11ba5894 refactor: reorganize layout structure by moving Header and Footer components to RootLayout 2025-05-11 05:27:19 +07:00
ArraysID 6d29c81dad feat: enhance LearningResourcesSection with primary and secondary button links 2025-05-11 05:26:07 +07:00
ArraysID 4a7135dd17 feat: add Learning Resources section with dynamic data fetching and UI integration 2025-05-11 05:24:31 +07:00
ArraysID 33d750b221 feat: add heroImage field to HeroSection and implement image formatting utility 2025-05-11 05:24:31 +07:00
ArraysID 1f9aa2e6b3 feat: update FeaturesSection to use Iconify icons and adjust icon properties 2025-05-11 05:24:31 +07:00
ArraysID 7722c98c84 feat: replace Link component with Button for improved button styling in Community component 2025-05-11 05:24:31 +07:00
ArraysID 0914361635 feat: update Community component to use Iconify for icons and adjust CommunitiesSection configuration 2025-05-11 05:24:31 +07:00
ArraysID 74aabd1eba feat: add support for s3 bucket for media 2025-05-11 05:24:31 +07:00
ArraysID d1fc7a8fd5 refactor: move global section functions to services directory 2025-05-11 05:24:31 +07:00
ArraysID 009cabfc1c feat: add CommunitiesSection configuration and integrate with Community component 2025-05-11 05:24:31 +07:00
ArraysID a7f7521761 fix: update revalidation interval to 10 seconds 2025-05-11 05:24:31 +07:00
ArraysID 54b7a2f0eb refactor: optimize data fetching by using Promise.all 2025-05-11 05:24:31 +07:00
ArraysID ef042f2f40 feat: add revalidate constant to control page revalidation interval 2025-05-11 05:24:31 +07:00
ArraysID fd35cb9525 feat: add FeaturesSection global configuration and integrate with Features component 2025-05-11 05:24:31 +07:00
ArraysID 18d11fd1c3 feat: implement HeroSection global configuration and integrate with Hero component 2025-05-11 05:24:31 +07:00
ArraysID c82d9bcc70 fix: include withPayload in Next.js plugins for proper configuration 2025-05-11 05:24:31 +07:00
ArraysID a4dd793bb7 refactor: update config import paths to use local payload.config 2025-05-11 05:24:31 +07:00
ArraysID bba3c89b23 fix: update TypeScript paths for payload configuration 2025-05-11 05:24:31 +07:00
ArraysID 8b6aa21f82 feat: add payload configuration and update TypeScript target 2025-05-11 05:24:31 +07:00
ArraysID 0fffe2d403 feat: integrate payload cms to landing page 2025-05-11 05:24:09 +07:00
Maulana SodiqinandGitHub 246adb57a1 chore: update action 2025-05-10 18:38:31 +07:00
Fahim ArdaniandGitHub 75839271ad fix: text color (#26) 2025-05-10 18:31:56 +07:00
ArraysID 919fac811d fix: remove unused environment variables from test build workflow 2025-05-10 17:07:51 +07:00
ArraysID 4316dcf8b3 chore: add environment placeholder variables for CMS configuration in test build workflow 2025-05-10 17:04:33 +07:00
ArraysID b9b71c4c25 fix: add placeholder to .env.example 2025-05-10 16:50:26 +07:00
ArraysID 121faf89c3 fix: supress scss deps warning 2025-05-10 16:26:32 +07:00
ArraysID 07913efb16 Merge branch 'develop' into feat/landing-cms 2025-05-10 16:01:05 +07:00
ArraysID 4b2733511c feat: wrap logo image in a link to navigate to the homepage 2025-05-10 15:50:42 +07:00
ArraysID 9f3d00a0db feat: update button components to use onClick for external links and improve styling 2025-05-10 15:48:35 +07:00
ArraysID 7ac571c019 feat: update CallToActionSection label and wrap Discord button in link 2025-05-10 15:34:18 +07:00
ArraysID a8c958400d feat: implement CallToAction section with dynamic data fetching and UI integration 2025-05-10 15:19:54 +07:00
ArraysID 760ab67ae0 feat: simplify joinTitle rendering in Testimonials section 2025-05-10 14:56:07 +07:00
ArraysID c4cf9e4e6c feat: implement Testimonials section with dynamic data fetching and UI integration 2025-05-10 14:52:35 +07:00
ArraysID a800b1e293 refactor: reorganize layout structure by moving Header and Footer components to RootLayout 2025-05-10 13:20:43 +07:00
ArraysID da0e599b9a feat: enhance LearningResourcesSection with primary and secondary button links 2025-05-10 12:58:34 +07:00
ArraysID a02a259f4e feat: add Learning Resources section with dynamic data fetching and UI integration 2025-05-10 12:48:07 +07:00
ArraysID ff99564b05 feat: add heroImage field to HeroSection and implement image formatting utility 2025-05-10 08:54:45 +07:00
ArraysID 13ab089afc feat: update FeaturesSection to use Iconify icons and adjust icon properties 2025-05-10 07:45:29 +07:00
ArraysID 378e258520 feat: replace Link component with Button for improved button styling in Community component 2025-05-10 07:30:06 +07:00
ArraysID 88468081b6 feat: update Community component to use Iconify for icons and adjust CommunitiesSection configuration 2025-05-10 07:26:38 +07:00
ArraysID aba462e462 feat: add support for s3 bucket for media 2025-05-10 06:55:11 +07:00
ArraysID 55fd1fa443 refactor: move global section functions to services directory 2025-05-10 05:38:01 +07:00
ArraysID 07a1eb8dbc feat: add CommunitiesSection configuration and integrate with Community component 2025-05-10 05:35:24 +07:00
ArraysID aad04b6f94 fix: update revalidation interval to 10 seconds 2025-05-10 04:33:06 +07:00
ArraysID d4a91abb63 refactor: optimize data fetching by using Promise.all 2025-05-10 04:29:46 +07:00
ArraysID 7fd5528c0d feat: add revalidate constant to control page revalidation interval 2025-05-10 04:29:28 +07:00
ArraysID 39a57b4fed feat: add FeaturesSection global configuration and integrate with Features component 2025-05-10 04:08:37 +07:00
ArraysID 7b490702b9 feat: implement HeroSection global configuration and integrate with Hero component 2025-05-10 03:32:19 +07:00
RasyidandGitHub 6e78b49731 feat: integrate payload cms to landing page (#25)
* feat: integrate payload cms to landing page

* feat: add payload configuration and update TypeScript target

* fix: update TypeScript paths for payload configuration

* refactor: update config import paths to use local payload.config

* fix: update quotation marks for testimonials to use HTML entities

* fix: include withPayload in Next.js plugins for proper configuration
2025-05-08 22:11:57 +07:00
ArraysID f31aad8570 fix: include withPayload in Next.js plugins for proper configuration 2025-05-08 16:07:51 +07:00
ArraysID acb7f4aef9 fix: update quotation marks for testimonials to use HTML entities 2025-05-08 15:42:24 +07:00
ArraysID c69614531b refactor: update config import paths to use local payload.config 2025-05-08 15:39:21 +07:00
ArraysID c82426282c fix: update TypeScript paths for payload configuration 2025-05-08 15:24:44 +07:00
ArraysID 45295953aa feat: add payload configuration and update TypeScript target 2025-05-08 15:16:59 +07:00
ArraysID 0561555276 feat: integrate payload cms to landing page 2025-05-08 15:12:32 +07:00
RasyidandGitHub 3aa552afd6 feat: landing page migration (#24)
* chore: cleanup nextjs project

* feat(shadcn-ui): initialize shadcn ui libs

* feat: update landing app with shadcnI integration

* feat: add path mapping for shadcn-ui atoms in tsconfig

* feat: remove unused tw-animate-css import

* feat: migrate header component

* feat: migrate hero component

* feat: migrate features component

* feat: migrate community component

* feat: migrate learning resources component

* feat: migrate testimonials component

* feat: migrate call to action component

* feat: migrate footer component

* feat: migrate simple theme switcher component

* feat: update dependencies in package.json and package-lock.json

* chore: add tailwind-animate-css package

* feat: initialize index file to export atoms

* chore: remove pnpm, accidentally added out of habit
2025-04-27 09:31:24 +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
egagofur 638b64b715 fix: validation by query param 2025-04-04 16:42:27 +07:00
egagofur d61cb4a215 fix: split zod validation register modal 2025-04-04 16:38:21 +07:00
Maulana SodiqinandGitHub 272db82c8c Merge pull request #18 from IMPHNEN/feat/register-integeration
feat: integeration register modal
2025-04-04 06:03:13 +07:00
121 changed files with 25141 additions and 4771 deletions
+14 -1
View File
@@ -1 +1,14 @@
VITE_API_URL=https://api.example.com # API Endpoint
VITE_API_URL=
# Payload CMS Secret Key (minimum 32 characters)
CMS_SECRET=your-very-long-secret-key
# PostgreSQL Connection String
CMS_POSTGRES_URL=postgres://user:password@host:port/database
# S3 Storage Configuration
CMS_STORAGE_ENDPOINT=https://your-s3-endpoint.com
CMS_STORAGE_ACCESS_KEY_ID=your-access-key-id
CMS_STORAGE_SECRET_ACCESS_KEY=your-secret-access-key
CMS_STORAGE_REGION=ap-southeast-1
+37
View File
@@ -0,0 +1,37 @@
name: Test and Build
on:
push:
branches: ['develop']
pull_request:
branches: ['develop']
env:
NODE_VERSION: 22.14.0
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: npm install
- name: Set env vars
run: |
echo "CMS_SECRET=${{ secrets.CMS_SECRET }}" >> $GITHUB_ENV
echo "CMS_STORAGE_REGION=${{ secrets.CMS_STORAGE_REGION }}" >> $GITHUB_ENV
echo "CMS_STORAGE_ENDPOINT=${{ secrets.CMS_STORAGE_ENDPOINT }}" >> $GITHUB_ENV
echo "CMS_STORAGE_ACCESS_KEY_ID=${{ secrets.CMS_STORAGE_ACCESS_KEY_ID }}" >> $GITHUB_ENV
echo "CMS_STORAGE_SECRET_ACCESS_KEY=${{ secrets.CMS_STORAGE_SECRET_ACCESS_KEY }}" >> $GITHUB_ENV
echo "CMS_POSTGRES_URL=${{ secrets.CMS_POSTGRES_URL }}" >> $GITHUB_ENV
- name: Nx Build
run: npx nx run-many --target=build --all
+11
View File
@@ -51,3 +51,14 @@ storybook-static
.env.prod .env.prod
.env.develop .env.develop
.env.staging .env.staging
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
# Next.js
.next
out
+2 -1
View File
@@ -2,6 +2,7 @@
"recommendations": [ "recommendations": [
"nrwl.angular-console", "nrwl.angular-console",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"dbaeumer.vscode-eslint" "dbaeumer.vscode-eslint",
"ms-playwright.playwright"
] ]
} }
+3 -6
View File
@@ -5,7 +5,7 @@ import {
usePostLogin, usePostLogin,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useNavigate } from 'react-router-dom'; import { toast } from 'sonner';
export const useLogin = () => { export const useLogin = () => {
const postLogin = usePostLogin(); const postLogin = usePostLogin();
@@ -13,14 +13,11 @@ export const useLogin = () => {
resolver: zodResolver(authLoginSchema), resolver: zodResolver(authLoginSchema),
mode: 'all', mode: 'all',
}); });
const navigate = useNavigate();
const onSubmit = form.handleSubmit((data) => { const onSubmit = form.handleSubmit((data) => {
postLogin.mutate(data, { postLogin.mutate(data, {
onSuccess: () => { onSuccess: () => toast.success("Login sukses"),
console.log('Success Login'); onError: (error) => toast.error(error.message),
navigate('/dashboard');
},
}); });
}); });
@@ -1,10 +1,12 @@
import { Button } from '@imphnen-frontend-service/ui/atoms'; 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 { interface IModalAddItem {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
handleAddItem: () => void; handleAddItem?: () => Promise<boolean>;
currentStep?: number; currentStep?: number;
nextStep: () => void; nextStep: () => void;
prevStep: () => void; prevStep: () => void;
@@ -46,91 +48,114 @@ interface IStepOneProps {
onClose: () => void; onClose: () => void;
} }
const StepOne = ({ nextStep }: IStepOneProps) => ( const StepOne = ({ nextStep }: IStepOneProps) => {
<> const { form, onSubmit } = useItem(nextStep);
<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>
<Button variant="primary" size="lg" className="w-full" onClick={nextStep}> return (
Tambahkan Item <>
</Button> <Modal.Header>
</Modal.Content> <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 { interface IStepTwoProps {
onClose: () => void; onClose: () => void;
handleAddItem?: () => void; handleAddItem?: () => Promise<boolean>;
resetStep: () => void; resetStep: () => void;
} }
const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => ( const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => {
<> const { onConfirm, onCancel } = useConfirmItem(
<Modal.Header className="mb-0 text-center items-center"> onClose,
<h2 className="text-p1 font-semibold text-primary-500 mb-3"> resetStep,
Tambah Item handleAddItem,
</h2> {
<p className="text-p3 text-neutral-400"> success: 'Item ditambahkan ke gacha item',
Apakah kamu yakin ingin error: 'Item gagal ditambahkan ke gacha item',
<br /> menambahkan item ini? }
</p> );
</Modal.Header>
<Modal.Content className="flex gap-4"> return (
<Button <>
variant="bordered" <Modal.Header className="mb-0 text-center items-center">
size="lg" <h2 className="text-p1 font-semibold text-primary-500 mb-3">
className="w-full" Tambah Item
onClick={() => { </h2>
onClose(); <p className="text-p3 text-neutral-400">
resetStep(); Apakah kamu yakin ingin
}} <br /> menambahkan item ini?
> </p>
Batal </Modal.Header>
</Button> <Modal.Content className="flex gap-4">
<Button <Button
variant="primary" variant="bordered"
size="lg" size="lg"
className="w-full" className="w-full"
onClick={() => { onClick={onCancel}
handleAddItem && handleAddItem(); >
onClose(); Batal
resetStep(); </Button>
}} <Button
> variant="primary"
Tambahkan size="lg"
</Button> className="w-full"
</Modal.Content> onClick={onConfirm}
</> >
); Tambahkan
</Button>
</Modal.Content>
</>
);
};
export default ModalAddItem; export default ModalAddItem;
@@ -1,10 +1,12 @@
import { Button } from '@imphnen-frontend-service/ui/atoms'; import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules'; 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 { interface IModalEditItem {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
handleEditItem?: () => void; handleEditItem?: () => Promise<boolean>;
currentStep?: number; currentStep?: number;
nextStep: () => void; nextStep: () => void;
prevStep: () => void; prevStep: () => void;
@@ -46,91 +48,117 @@ interface IStepOneProps {
onClose: () => void; onClose: () => void;
} }
const StepOne = ({ nextStep }: IStepOneProps) => ( const StepOne = ({ nextStep }: IStepOneProps) => {
<> const initialValues = {
<Modal.Header> itemName: 'Hoodie IMPHNEN Official 2025',
<h2 className="text-p1 font-semibold text-primary-500 mb-3"> quantity: 10,
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>
<Button variant="primary" size="lg" className="w-full" onClick={nextStep}> const { form, onSubmit } = useItem(nextStep, initialValues);
Perbarui Item
</Button> return (
</Modal.Content> <>
</> <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 { interface IStepTwoProps {
onClose: () => void; onClose: () => void;
handleEditItem?: () => void; handleEditItem?: () => Promise<boolean>;
resetStep: () => void; resetStep: () => void;
} }
const StepTwo = ({ onClose, handleEditItem, resetStep }: IStepTwoProps) => ( const StepTwo = ({ onClose, handleEditItem, resetStep }: IStepTwoProps) => {
<> const { onConfirm, onCancel } = useConfirmItem(
<Modal.Header className="mb-0 text-center items-center"> onClose,
<h2 className="text-p1 font-semibold text-primary-500 mb-3"> resetStep,
Update Item handleEditItem,
</h2> {
<p className="text-p3 text-neutral-400"> success: 'Perubahan item berhasil dilakukan',
Apakah kamu yakin dengan error: 'Perubahan item gagal dilakukan',
<br /> perubahan yang dilakukan? }
</p> );
</Modal.Header> return (
<Modal.Content className="flex gap-4"> <>
<Button <Modal.Header className="mb-0 text-center items-center">
variant="bordered" <h2 className="text-p1 font-semibold text-primary-500 mb-3">
size="lg" Update Item
className="w-full" </h2>
onClick={() => { <p className="text-p3 text-neutral-400">
onClose(); Apakah kamu yakin dengan
resetStep(); <br /> perubahan yang dilakukan?
}} </p>
> </Modal.Header>
Batal <Modal.Content className="flex gap-4">
</Button> <Button
<Button variant="bordered"
variant="primary" size="lg"
size="lg" className="w-full"
className="w-full" onClick={onCancel}
onClick={() => { >
handleEditItem && handleEditItem(); Batal
onClose(); </Button>
resetStep(); <Button
}} variant="primary"
> size="lg"
Update className="w-full"
</Button> onClick={onConfirm}
</Modal.Content> >
</> Update
); </Button>
</Modal.Content>
</>
);
};
export default ModalEditItem; 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 ( return (
<Fragment> <Fragment>
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8"> <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"> <header className="bg-white py-4 px-8 rounded-lg shadow p-4">
<h1 className="text-p2 font-semibold">Dashboard</h1> <h1 className="text-p2 font-semibold">Dashboard</h1>
</header> </header>
<div className="flex justify-between gap-[40px] p-8 bg-white rounded-md"> <div className="flex justify-between gap-[40px] p-8 bg-white rounded-md">
<div className="w-full flex flex-col gap-[40px]"> <div className="w-full flex flex-col gap-[40px]">
{/* Summary Section */}
<section> <section>
<h2 className="text-p2 font-medium text-primary-500 mb-8"> <h2 className="text-p2 font-medium text-primary-500 mb-8">
Summary Summary
</h2> </h2>
<div className="grid grid-cols-2 gap-4"> <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="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"> <div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
<UsergroupAddOutlined className="text-[20px]" /> <UsergroupAddOutlined className="text-[20px]" />
@@ -55,7 +52,6 @@ export const Components: FC = (): ReactElement => {
</div> </div>
</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="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"> <div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
<ReloadOutlined className="text-[20px]" /> <ReloadOutlined className="text-[20px]" />
@@ -68,7 +64,6 @@ export const Components: FC = (): ReactElement => {
</div> </div>
</div> </div>
{/* Redeem */}
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100"> <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"> <div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
<UserSwitchOutlined className="text-[20px]" /> <UserSwitchOutlined className="text-[20px]" />
@@ -79,7 +74,6 @@ export const Components: FC = (): ReactElement => {
</div> </div>
</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="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"> <div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
<UsergroupDeleteOutlined className="text-[20px]" /> <UsergroupDeleteOutlined className="text-[20px]" />
@@ -94,7 +88,6 @@ export const Components: FC = (): ReactElement => {
</div> </div>
</section> </section>
{/* Gacha Items Section */}
<section className="flex flex-col gap-8"> <section className="flex flex-col gap-8">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h2 className="text-p2 font-medium text-primary-500"> <h2 className="text-p2 font-medium text-primary-500">
@@ -111,21 +104,20 @@ export const Components: FC = (): ReactElement => {
</Button> </Button>
</div> </div>
{/* Gacha Items List */}
<div className="flex flex-col gap-4 max-h-140 overflow-auto"> <div className="flex flex-col gap-4 max-h-140 overflow-auto">
{[1, 2, 3, 4, 5, 6].map((item) => ( {[1, 2, 3, 4, 5, 6].map((item) => (
<div <div
key={item} 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> <div>
<h3 className="text-p3 text-primary-500 font-medium"> <h3 className="text-p3 text-primary-500 font-medium">
Lanyard IMPHNEN Lanyard IMPHNEN
</h3> </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>Prize {item}</span>
<span>Chance Rate: (0.1%)</span> <span>Quantity: 10</span>
</div> </div>
</div> </div>
<div className="flex justify-start gap-2"> <div className="flex justify-start gap-2">
@@ -148,12 +140,7 @@ export const Components: FC = (): ReactElement => {
</div> </div>
</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" />
<img
src="gacha-clip.webp"
alt="Lanyard IMPHNEN"
className="h-full"
/>
</div> </div>
))} ))}
</div> </div>
@@ -174,9 +161,6 @@ export const Components: FC = (): ReactElement => {
currentStep={currentStep} currentStep={currentStep}
isOpen={showModalAddItem} isOpen={showModalAddItem}
onClose={() => setShowModalAddItem(false)} onClose={() => setShowModalAddItem(false)}
handleAddItem={() => {
console.log('Item added');
}}
nextStep={nextStep} nextStep={nextStep}
prevStep={prevStep} prevStep={prevStep}
resetStep={resetStep} resetStep={resetStep}
@@ -187,9 +171,6 @@ export const Components: FC = (): ReactElement => {
currentStep={currentStep} currentStep={currentStep}
isOpen={showModalEditItem} isOpen={showModalEditItem}
onClose={() => setShowModalEditItem(false)} onClose={() => setShowModalEditItem(false)}
handleEditItem={() => {
console.log('Item edited');
}}
nextStep={nextStep} nextStep={nextStep}
prevStep={prevStep} prevStep={prevStep}
resetStep={resetStep} resetStep={resetStep}
@@ -199,9 +180,6 @@ export const Components: FC = (): ReactElement => {
<ModalDeleteItem <ModalDeleteItem
isOpen={showModalDeleteItem} isOpen={showModalDeleteItem}
onClose={() => setShowModalDeleteItem(false)} onClose={() => setShowModalDeleteItem(false)}
handleDeleteItem={() => {
console.log('Item deleted');
}}
/> />
</Fragment> </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, convertPagesToRoute,
QueryProvider, QueryProvider,
} from '@imphnen-frontend-service/utils'; } from '@imphnen-frontend-service/utils';
import { Toaster } from 'sonner';
import './index.css'; import './index.css';
const files = import.meta.glob('./app/**/*(page|layout).tsx'); const files = import.meta.glob('./app/**/*(page|layout).tsx');
@@ -34,6 +35,7 @@ if (!rootElement) throw new Error('Failed to find the root element');
createRoot(rootElement).render( createRoot(rootElement).render(
<StrictMode> <StrictMode>
<QueryProvider> <QueryProvider>
<Toaster position="top-right" />
<RouterProvider router={router} /> <RouterProvider router={router} />
</QueryProvider> </QueryProvider>
</StrictMode> </StrictMode>
@@ -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 { FC, ReactElement } from 'react';
import { LoginBanner } from '@imphnen-frontend-service/ui/organisms'; import { ControlledInputField, LoginBanner } from '@imphnen-frontend-service/ui/organisms';
import { Button } from '@imphnen-frontend-service/ui/atoms'; import { Button } from '@imphnen-frontend-service/ui/atoms';
import { useSearchParams } from 'react-router-dom'; import { useLogin } from '../../_hooks/use-login';
import { InputField } from '@imphnen-frontend-service/ui/molecules'; import { ArrowLeftOutlined } from '@ant-design/icons';
export const Components: FC = (): ReactElement => { export const Components: FC = (): ReactElement => {
const [searchParams] = useSearchParams(); const { form, onSubmit } = useLogin();
const error = searchParams.get('error'); // Ambil nilai ?error=
return ( return (
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]"> <div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
<div className="bg-white min-w-[1120px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6"> <div className="bg-white xl:min-w-[1120px] xl:min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6">
<LoginBanner /> <LoginBanner />
<div className="border-2 border-primary-500/50 w-[596px] rounded-lg py-[70px] px-[96px] flex justify-center"> <div className="xl:border-2 xl:border-primary-500/50 md:w-[596px] rounded-lg md:py-[70px] md:px-[96px] flex justify-center">
<div className="w-[404px]"> <div className="md:w-[404px]">
<h2 className="text-4xl font-semibold text-primary-500 text-center mb-2"> <h2 className="text-4xl font-semibold text-primary-500 text-center mb-2">
Hallo Minna-san Hallo Minna-san
</h2> </h2>
<h5 className="text-xl font-medium text-primary-500 text-center"> <h5 className="text-xl font-medium text-primary-500 text-center mb-5">
Welcome to Dimentorin by IMPHNEN Welcome to Dimentorin by IMPHNEN
</h5> </h5>
<InputField <form onSubmit={onSubmit} className="flex flex-col gap-2">
label="Email" <ControlledInputField
error={error ? error : undefined} control={form.control}
size="lg" label="Email"
className="w-full" size="lg"
placeholder="Masukkan email-mu, Senpai~! ✨ (Pastikan tidak typo, ya~ 😆)" className="w-full mb-2"
/> placeholder="Masukkan email-mu, Senpai~! ✨ (Pastikan tidak typo, ya~ 😆)"
<InputField name={'email'}
label="password" />
error={error ? error : undefined} <ControlledInputField
className="w-full" control={form.control}
size="lg" label="Password"
type="password" className="w-full"
placeholder="Masukkan password rahasiamu!" size="lg"
/> type="password"
<div className="flex justify-end my-5"> placeholder="Masukkan password rahasiamu!"
<a href="/auth/forgot" className="text-primary-500 font-medium"> name={'password'}
Lupa Password ? />
</a> <div className="flex justify-end my-5">
</div> <a href="/auth/forgot" className="text-primary-500 font-medium">
<Button className="w-full">Enter Isekai</Button> Lupa Password ?
</a>
</div>
<Button className="w-full" type='submit'>Enter Isekai</Button>
</form>
<div className="flex my-3 gap-3 justify-center"> <div className="flex my-3 gap-3 justify-center">
<h5>Belum Punya akun ?</h5> <h5>Belum Punya akun ?</h5>
<a href="/auth/register" className="text-primary-500 font-medium"> <a href="/auth/register" className="text-primary-500 font-medium">
@@ -63,6 +67,13 @@ export const Components: FC = (): ReactElement => {
width={24} width={24}
/> />
</Button> </Button>
<Button
className='xl:hidden w-full gap-3'
variant='secondary'
>
<ArrowLeftOutlined />
Kembali Ke Homepage
</Button>
</div> </div>
</div> </div>
</div> </div>
+87 -5
View File
@@ -1,14 +1,96 @@
import { FC, ReactElement } from 'react'; 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 { Button, Input } from '@imphnen-frontend-service/ui/atoms';
import { InputField } from '@imphnen-frontend-service/ui/molecules';
export const Components: FC = (): ReactElement => { export const Components: FC = (): ReactElement => {
return ( return (
<div className='flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]'> <div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
<div className='bg-white min-w-[1120px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6'> <div className="bg-white min-w-[1120px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6">
<RegisterResetBanner /> <RegisterResetBanner />
<div className='border-2 border-primary-500/50 w-[596px] rounded-lg py-[70px] px-[96px] flex justify-center'> <div className="border-2 border-primary-500/50 w-[696px] rounded-lg py-[32px] px-[48px] flex justify-center">
<div className='w-[404px]'> <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>
</div> </div>
@@ -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, convertPagesToRoute,
QueryProvider, QueryProvider,
} from '@imphnen-frontend-service/utils'; } from '@imphnen-frontend-service/utils';
import { Toaster } from 'sonner';
import './index.css'; import './index.css';
const files = import.meta.glob('./app/**/*(page|layout).tsx'); const files = import.meta.glob('./app/**/*(page|layout).tsx');
@@ -34,6 +35,7 @@ if (!rootElement) throw new Error('Failed to find the root element');
createRoot(rootElement).render( createRoot(rootElement).render(
<StrictMode> <StrictMode>
<QueryProvider> <QueryProvider>
<Toaster position="top-right" />
<RouterProvider router={router} /> <RouterProvider router={router} />
</QueryProvider> </QueryProvider>
</StrictMode> </StrictMode>
@@ -5,6 +5,7 @@ import { useRegister } from '../../_hooks/use-register';
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'; import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
import { UseFormReturn } from 'react-hook-form'; import { UseFormReturn } from 'react-hook-form';
import { TRegisterRequest } from '@imphnen-frontend-service/service'; import { TRegisterRequest } from '@imphnen-frontend-service/service';
import { useEffect } from 'react';
interface IModalFormRegisterProps { interface IModalFormRegisterProps {
isOpen: boolean; isOpen: boolean;
@@ -12,7 +13,7 @@ interface IModalFormRegisterProps {
} }
const ModalFormRegister = ({ isOpen, onClose }: IModalFormRegisterProps) => { const ModalFormRegister = ({ isOpen, onClose }: IModalFormRegisterProps) => {
const { form, onSubmit } = useRegister(); const { form, onSubmit, isStepOneValid } = useRegister();
const { const {
step: currentStep, step: currentStep,
@@ -25,6 +26,12 @@ const ModalFormRegister = ({ isOpen, onClose }: IModalFormRegisterProps) => {
minValue: 1, minValue: 1,
}); });
useEffect(() => {
if (!isStepOneValid && currentStep === 2) {
prevStep();
}
}, [isStepOneValid, currentStep, prevStep]);
return ( return (
<Modal <Modal
className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7" className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7"
@@ -46,7 +53,11 @@ const ModalFormRegister = ({ isOpen, onClose }: IModalFormRegisterProps) => {
<Modal.Content> <Modal.Content>
<form onSubmit={onSubmit} className="space-y-6"> <form onSubmit={onSubmit} className="space-y-6">
{currentStep === 1 && ( {currentStep === 1 && (
<StepOne form={form} nextStep={nextStep} onClose={onClose} /> <StepOne
form={form}
nextStep={nextStep}
isStepOneValid={isStepOneValid}
/>
)} )}
{currentStep === 2 && <StepTwo form={form} prevStep={prevStep} />} {currentStep === 2 && <StepTwo form={form} prevStep={prevStep} />}
</form> </form>
@@ -58,52 +69,59 @@ const ModalFormRegister = ({ isOpen, onClose }: IModalFormRegisterProps) => {
interface IStepOneProps { interface IStepOneProps {
form: UseFormReturn<TRegisterRequest, any, TRegisterRequest>; form: UseFormReturn<TRegisterRequest, any, TRegisterRequest>;
nextStep: () => void; nextStep: () => void;
onClose: () => void; isStepOneValid: boolean;
} }
const StepOne = ({ form, nextStep, onClose }: IStepOneProps) => ( const StepOne = ({ form, nextStep, isStepOneValid }: IStepOneProps) => {
<> return (
<ControlledInputField <>
control={form.control} <ControlledInputField
name="fullname" control={form.control}
label="Nama Lengkap" name="fullname"
placeholder="Masukkan Nama Lengkap" label="Nama Lengkap"
type="text" placeholder="Masukkan Nama Lengkap"
size="lg" type="text"
className="w-full" size="lg"
/> className="w-full"
<ControlledInputField />
control={form.control} <ControlledInputField
label="Email" control={form.control}
placeholder="Masukkan Email Anda" label="Email"
type="email" placeholder="Masukkan Email Anda"
name="email" type="email"
size="lg" name="email"
className="w-full" size="lg"
/> className="w-full"
<ControlledInputField />
control={form.control} <ControlledInputField
name="password" control={form.control}
label="Password" name="password"
placeholder="Masukkan Password" label="Password"
type="password" placeholder="Masukkan Password"
size="lg" type="password"
className="w-full" size="lg"
/> className="w-full"
<ControlledInputField />
control={form.control} <ControlledInputField
name="confirm_password" control={form.control}
label="Ulangi Password" name="confirm_password"
placeholder="Masukkan Ulang Password" label="Ulangi Password"
type="password" placeholder="Masukkan Ulang Password"
size="lg" type="password"
className="w-full" size="lg"
/> className="w-full"
<Button size="md" className="w-full" onClick={nextStep}> />
Selanjutnya <Button
</Button> size="md"
</> className="w-full"
); onClick={nextStep}
disabled={!isStepOneValid}
>
Selanjutnya
</Button>
</>
);
};
interface IStepTwoProps { interface IStepTwoProps {
form: UseFormReturn<TRegisterRequest, any, TRegisterRequest>; form: UseFormReturn<TRegisterRequest, any, TRegisterRequest>;
+30
View File
@@ -1,19 +1,48 @@
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { import {
authRegisterSchema, authRegisterSchema,
stepOneRegisterSchema,
TRegisterRequest, TRegisterRequest,
usePostRegister, usePostRegister,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useEffect, useState } from 'react';
export const useRegister = () => { export const useRegister = () => {
const postRegister = usePostRegister(); const postRegister = usePostRegister();
const form = useForm<TRegisterRequest>({ const form = useForm<TRegisterRequest>({
resolver: zodResolver(authRegisterSchema), resolver: zodResolver(authRegisterSchema),
mode: 'all', mode: 'all',
defaultValues: {
fullname: '',
email: '',
password: '',
confirm_password: '',
phone_number: '',
referral_code: '',
referred_by: '',
student_type: '',
},
}); });
const [stepValid, setStepValid] = useState(false);
useEffect(() => {
const subscription = form.watch(() => {
const { fullname, email, password, confirm_password } = form.getValues();
const valid = stepOneRegisterSchema.safeParse({
fullname,
email,
password,
confirm_password,
}).success;
setStepValid(valid);
});
return () => subscription.unsubscribe();
}, [form]);
const onSubmit = form.handleSubmit((data) => { const onSubmit = form.handleSubmit((data) => {
postRegister.mutate(data, { postRegister.mutate(data, {
onSuccess: (data) => toast.success(data.message), onSuccess: (data) => toast.success(data.message),
@@ -24,5 +53,6 @@ export const useRegister = () => {
return { return {
form, form,
onSubmit, onSubmit,
isStepOneValid: stepValid,
}; };
}; };
+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.
+15
View File
@@ -0,0 +1,15 @@
const { composePlugins, withNx } = require('@nx/next');
const { withPayload } = require('@payloadcms/next/withPayload');
/** @type {import('@nx/next/plugins/with-nx').WithNxOptions} */
const nextConfig = {
nx: { svgr: false },
sassOptions: {
quietDeps: true,
logger: {
warn: () => {},
},
},
};
module.exports = composePlugins(withNx, withPayload)(nextConfig);
+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: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

@@ -0,0 +1,78 @@
'use client';
import { Button } from '@imphnen-frontend-service/shadcn-ui/atoms';
import { CallToActionSection } from 'apps/landing/src/payload-types';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
export function CallToAction(props: CallToActionSection) {
const {
title,
highlightedTitle,
subtitle,
primaryButtonLabel,
primaryButtonLink,
secondaryButtonLabel,
secondaryButtonLink,
} = props;
const ref = useRef(null);
const isInView = useInView(ref, { once: true, amount: 0.2 });
return (
<section className="w-full py-20 md:py-32 relative overflow-hidden">
{/* Background Elements */}
<div className="absolute inset-0 -z-10">
<div className="absolute inset-0 bg-gradient-to-b from-background to-primary/20" />
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(59,130,246,0.2),transparent_70%)]" />
</div>
<div className="container px-4 md:px-6" ref={ref}>
<motion.div
className="max-w-4xl mx-auto rounded-2xl overflow-hidden border shadow-lg"
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
transition={{ duration: 0.5 }}
>
<div className="relative p-8 md:p-12 lg:p-16 bg-background">
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-blue-400/5" />
<div className="relative z-10 text-center">
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold mb-6">
{title}{' '}
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
{highlightedTitle}
</span>
</h2>
<p className="text-lg text-muted-foreground mb-8 max-w-2xl mx-auto">
{subtitle}
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button
size="lg"
className="bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90 font-bold text-black hover:text-white cursor-pointer"
onClick={() => window.open(primaryButtonLink, '_blank')}
>
{primaryButtonLabel}
</Button>
<Button
size="lg"
variant="outline"
className="border-primary hover:bg-primary/10"
onClick={() => window.open(secondaryButtonLink, '_blank')}
>
{secondaryButtonLabel}
</Button>
</div>
</div>
{/* Decorative Elements */}
<div className="absolute -top-12 -left-12 w-24 h-24 rounded-full bg-primary/10 blur-2xl" />
<div className="absolute -bottom-12 -right-12 w-24 h-24 rounded-full bg-blue-400/10 blur-2xl" />
</div>
</motion.div>
</div>
</section>
);
}
@@ -0,0 +1,110 @@
'use client';
import { Icon } from '@iconify/react';
import { Button } from '@imphnen-frontend-service/shadcn-ui/atoms';
import { CommunitiesSection } from 'apps/landing/src/payload-types';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
export function Community(props: CommunitiesSection) {
const { items, stats } = props;
const ref = useRef(null);
const isInView = useInView(ref, { once: true, amount: 0.2 });
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: { duration: 0.5 },
},
};
return (
<section
id="komunitas"
className="w-full py-20 md:py-32 bg-muted relative overflow-hidden"
>
<div className="absolute inset-0 -z-10">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(59,130,246,0.1),transparent_70%)]" />
<div className="absolute inset-0 bg-[linear-gradient(to_right,rgba(59,130,246,0.01)_1px,transparent_1px),linear-gradient(to_bottom,rgba(59,130,246,0.01)_1px,transparent_1px)] bg-[size:14px_14px]" />
</div>
<div className="container px-4 md:px-6" ref={ref}>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
>
<h2 className="text-3xl font-bold tracking-tighter md:text-4xl/tight lg:text-5xl text-center mb-4">
Komunitas{' '}
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
Kami
</span>
</h2>
<p className="max-w-[800px] mx-auto text-muted-foreground text-center md:text-lg">
Bergabunglah dengan ribuan programmer Indonesia yang saling membantu
dan berbagi pengalaman.
</p>
</motion.div>
<motion.div
className="grid gap-8 md:grid-cols-3 mt-12"
variants={containerVariants}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
>
{items?.map((c, i) => (
<motion.div
key={i}
className="group relative overflow-hidden rounded-xl border bg-background p-6 transition-all hover:shadow-xl"
variants={itemVariants}
>
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-blue-400/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
<div className="relative z-10">
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900">
<Icon
icon={c.iconName}
className="h-6 w-6 text-blue-600 dark:text-blue-400"
/>
</div>
<h3 className="mb-2 text-xl font-bold">{c.title}</h3>
<p className="mb-6 text-muted-foreground">{c.description}</p>
<Button
variant="outline"
className="w-full group-hover:bg-primary group-hover:text-primary-foreground transition-colors duration-300"
onClick={() => window.open(c.buttonLink, '_blank')}
>
{c.buttonText}
</Button>
</div>
<div className="absolute -bottom-1 -right-1 w-20 h-20 bg-gradient-to-tl from-primary/20 to-transparent rounded-tl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
</motion.div>
))}
</motion.div>
<motion.div
className="mt-20 grid grid-cols-2 md:grid-cols-4 gap-8 text-center"
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.3 }}
>
{stats?.map((s, i) => (
<div key={i} className="space-y-2">
<div className="text-4xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
{s.value}
</div>
<div className="text-sm text-muted-foreground">{s.label}</div>
</div>
))}
</motion.div>
</div>
</section>
);
}
@@ -0,0 +1,95 @@
'use client';
import { Icon } from '@iconify/react';
import { FeaturesSection } from 'apps/landing/src/payload-types';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
export function Features(props: FeaturesSection) {
const { heading, subheading, features } = props;
const ref = useRef(null);
const isInView = useInView(ref, { once: true, amount: 0.2 });
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: { duration: 0.5 },
},
};
return (
<section
id="fitur"
className="w-full py-20 md:py-32 relative overflow-hidden"
>
<div className="absolute inset-0 -z-10">
<div className="absolute inset-0 bg-gradient-to-b from-background via-background to-muted/50" />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] rounded-full bg-gradient-to-tr from-primary/5 to-blue-400/5 blur-3xl" />
</div>
<div className="container px-4 md:px-6" ref={ref}>
<div className="flex flex-col items-center justify-center space-y-4 text-center mb-16">
<motion.div
className="space-y-2"
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
transition={{ duration: 0.5 }}
>
<div className="inline-block rounded-full bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary">
Fitur Unggulan
</div>
<h2 className="text-3xl font-bold tracking-tighter md:text-4xl/tight lg:text-5xl">
{heading}
</h2>
<p className="max-w-[800px] mx-auto text-muted-foreground md:text-lg">
{subheading}
</p>
</motion.div>
</div>
<motion.div
className="grid gap-8 md:grid-cols-2 lg:grid-cols-4"
variants={containerVariants}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
>
{features?.map((feature, index) => (
<motion.div
key={index}
className="group relative overflow-hidden rounded-xl border bg-background/50 backdrop-blur-sm p-6 transition-all hover:shadow-md hover:shadow-primary/5 hover:border-primary/50"
variants={itemVariants}
>
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-blue-400/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
<div className="relative z-10">
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 group-hover:bg-primary/20 transition-colors">
<Icon
icon={feature.iconName}
className="h-6 w-6 text-primary"
/>
</div>
<h3 className="mb-2 text-xl font-bold">{feature.title}</h3>
<p className="text-muted-foreground">{feature.description}</p>
</div>
<div className="absolute bottom-0 left-0 h-1 w-0 bg-gradient-to-r from-primary to-blue-400 group-hover:w-full transition-all duration-300" />
</motion.div>
))}
</motion.div>
</div>
</section>
);
}
@@ -0,0 +1,208 @@
import Link from 'next/link';
export default function Footer() {
return (
<footer className="w-full border-t bg-background py-12 md:py-16">
<div className="container px-4 md:px-6">
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-4">
<div className="space-y-4">
<div className="flex items-center gap-2">
<span className="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
IMPHNEN
</span>
</div>
<p className="text-sm text-muted-foreground">
Ingin Menjadi Programmer Handal merupakan komunitas Programmer
Indonesia.
</p>
<div className="flex space-x-4">
<Link
href="#"
className="text-muted-foreground hover:text-foreground"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z" />
</svg>
</Link>
<Link
href="#"
className="text-muted-foreground hover:text-foreground"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M9 8h-3v4h3v12h5v-12h3.642l.358-4h-4v-1.667c0-.955.192-1.333 1.115-1.333h2.885v-5h-3.808c-3.596 0-5.192 1.583-5.192 4.615v3.385z" />
</svg>
</Link>
<Link
href="#"
className="text-muted-foreground hover:text-foreground"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z" />
</svg>
</Link>
<Link
href="#"
className="text-muted-foreground hover:text-foreground"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M19.615 3.184c-3.604-.246-11.631-.245-15.23 0-3.897.266-4.356 2.62-4.385 8.816.029 6.185.484 8.549 4.385 8.816 3.6.245 11.626.246 15.23 0 3.897-.266 4.356-2.62 4.385-8.816-.029-6.185-.484-8.549-4.385-8.816zm-10.615 12.816v-8l8 3.993-8 4.007z" />
</svg>
</Link>
</div>
</div>
<div className="space-y-4">
<h3 className="text-lg font-bold">Tautan Cepat</h3>
<ul className="space-y-2">
<li>
<Link
href="#fitur"
className="text-sm text-muted-foreground hover:text-foreground"
>
Fitur
</Link>
</li>
<li>
<Link
href="#komunitas"
className="text-sm text-muted-foreground hover:text-foreground"
>
Komunitas
</Link>
</li>
<li>
<Link
href="#sumber-belajar"
className="text-sm text-muted-foreground hover:text-foreground"
>
Sumber Belajar
</Link>
</li>
<li>
<Link
href="#testimoni"
className="text-sm text-muted-foreground hover:text-foreground"
>
Testimoni
</Link>
</li>
</ul>
</div>
<div className="space-y-4">
<h3 className="text-lg font-bold">Sumber Belajar</h3>
<ul className="space-y-2">
<li>
<Link
href="#"
className="text-sm text-muted-foreground hover:text-foreground"
>
Video Tutorial
</Link>
</li>
<li>
<Link
href="#"
className="text-sm text-muted-foreground hover:text-foreground"
>
Artikel
</Link>
</li>
<li>
<Link
href="#"
className="text-sm text-muted-foreground hover:text-foreground"
>
Tantangan Koding
</Link>
</li>
<li>
<Link
href="#"
className="text-sm text-muted-foreground hover:text-foreground"
>
Sharing Session
</Link>
</li>
</ul>
</div>
<div className="space-y-4">
<h3 className="text-lg font-bold">Bahasa Pemrograman</h3>
<div className="flex flex-wrap gap-2">
<span className="inline-flex items-center rounded-md bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-600/10 dark:bg-blue-900/30 dark:text-blue-400 dark:ring-blue-400/20">
PHP
</span>
<span className="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-yellow-600/10 dark:bg-yellow-900/30 dark:text-yellow-400 dark:ring-yellow-400/20">
JavaScript
</span>
<span className="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/10 dark:bg-green-900/30 dark:text-green-400 dark:ring-green-400/20">
Python
</span>
<span className="inline-flex items-center rounded-md bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-600/10 dark:bg-blue-900/30 dark:text-blue-400 dark:ring-blue-400/20">
C#
</span>
<span className="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-600/10 dark:bg-red-900/30 dark:text-red-400 dark:ring-red-400/20">
Java
</span>
<span className="inline-flex items-center rounded-md bg-cyan-50 px-2 py-1 text-xs font-medium text-cyan-700 ring-1 ring-inset ring-cyan-600/10 dark:bg-cyan-900/30 dark:text-cyan-400 dark:ring-cyan-400/20">
Go
</span>
<span className="inline-flex items-center rounded-md bg-orange-50 px-2 py-1 text-xs font-medium text-orange-700 ring-1 ring-inset ring-orange-600/10 dark:bg-orange-900/30 dark:text-orange-400 dark:ring-orange-400/20">
Rust
</span>
<span className="inline-flex items-center rounded-md bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-600/10 dark:bg-blue-900/30 dark:text-blue-400 dark:ring-blue-400/20">
HTML
</span>
</div>
<div className="mt-6">
<h3 className="text-lg font-bold mb-2">Newsletter</h3>
<p className="text-sm text-muted-foreground mb-2">
Dapatkan update terbaru dari kami
</p>
<div className="flex gap-2">
<input
type="email"
placeholder="Email Anda"
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
/>
<button className="inline-flex items-center justify-center rounded-md bg-primary px-3 py-1 text-sm shadow hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50
font-bold text-black hover:text-white cursor-pointer">
Daftar
</button>
</div>
</div>
</div>
</div>
<div className="mt-8 border-t pt-8 text-center">
<p className="text-xs text-muted-foreground">
© {new Date().getFullYear()} IMPHNEN - Ingin Menjadi Programmer
Handal, Namun Enggan Ngoding. All rights reserved.
</p>
</div>
</div>
</footer>
);
}
@@ -0,0 +1,159 @@
'use client';
import {
Button,
MenuIcon,
XIcon,
} from '@imphnen-frontend-service/shadcn-ui/atoms';
import { cn } from '@imphnen-frontend-service/utils';
import Image from 'next/image';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { SimpleThemeToggle } from './simple-theme-toggle';
export function Header() {
const [isScrolled, setIsScrolled] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 10);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<header
className={cn(
'sticky top-0 z-50 w-full transition-all duration-300',
isScrolled
? 'bg-background/80 backdrop-blur-md border-b shadow-sm'
: 'bg-transparent'
)}
>
<div className="container flex h-16 items-center justify-between">
<div className="flex items-center gap-2">
<div className="relative overflow-hidden rounded">
<Link href="/">
<Image
src="/logo.png"
alt="IMPHNEN"
width={64}
height={64}
className="object-cover"
/>
</Link>
</div>
</div>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center gap-8">
<Link href="#fitur" className="text-sm font-medium relative group">
<span className="transition-colors hover:text-primary">Fitur</span>
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
</Link>
<Link
href="#komunitas"
className="text-sm font-medium relative group"
>
<span className="transition-colors hover:text-primary">
Komunitas
</span>
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
</Link>
<Link
href="#sumber-belajar"
className="text-sm font-medium relative group"
>
<span className="transition-colors hover:text-primary">
Sumber Belajar
</span>
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
</Link>
<Link
href="#testimoni"
className="text-sm font-medium relative group"
>
<span className="transition-colors hover:text-primary">
Testimoni
</span>
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
</Link>
</nav>
<div className="flex items-center gap-4">
<SimpleThemeToggle />
<Button
className="hidden md:flex bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90 transition-all duration-300 font-bold text-black hover:text-white cursor-pointer"
onClick={() =>
window.open('https://discord.com/invite/imphnen', '_blank')
}
>
Gabung Discord
</Button>
{/* Mobile Menu Button */}
<Button
variant="ghost"
size="icon"
className="md:hidden"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
>
{mobileMenuOpen ? (
<XIcon className="h-6 w-6" />
) : (
<MenuIcon className="h-6 w-6" />
)}
</Button>
</div>
</div>
{/* Mobile Menu */}
{mobileMenuOpen && (
<div className="md:hidden border-t bg-background/95 backdrop-blur-md">
<nav className="container flex flex-col py-4 text-center">
<Link
href="#fitur"
className="py-3 text-sm font-medium border-b border-border/50"
onClick={() => setMobileMenuOpen(false)}
>
Fitur
</Link>
<Link
href="#komunitas"
className="py-3 text-sm font-medium border-b border-border/50"
onClick={() => setMobileMenuOpen(false)}
>
Komunitas
</Link>
<Link
href="#sumber-belajar"
className="py-3 text-sm font-medium border-b border-border/50"
onClick={() => setMobileMenuOpen(false)}
>
Sumber Belajar
</Link>
<Link
href="#testimoni"
className="py-3 text-sm font-medium"
onClick={() => setMobileMenuOpen(false)}
>
Testimoni
</Link>
<a
href="https://discord.com/invite/imphnen"
target="_blank"
className="mt-4"
>
<Button className="mt-4 bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90 font-bold text-black hover:text-white cursor-pointer">
Gabung Discord
</Button>
</a>
</nav>
</div>
)}
</header>
);
}
@@ -0,0 +1,154 @@
'use client';
import {
Button,
CodeIcon,
SparklesIcon,
UsersIcon,
} from '@imphnen-frontend-service/shadcn-ui/atoms';
import { formatCMSImageDataToMedia } from 'apps/landing/src/lib/format';
import { HeroSection } from 'apps/landing/src/payload-types';
import { motion } from 'framer-motion';
import Image from 'next/image';
import { Fragment, useEffect, useState } from 'react';
export function Hero(props: HeroSection) {
const {
badgeText,
buttons,
description,
highlight,
title,
stats,
heroImage,
} = props;
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handleScroll = () => {
setScrollY(window.scrollY);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<section className="relative w-full py-20 md:py-32 lg:py-40 overflow-hidden">
<div className="absolute inset-0 -z-10 overflow-hidden">
<div className="absolute top-0 left-0 w-full h-full bg-gradient-to-b from-background to-background/50" />
<div
className="absolute top-1/4 -left-20 w-80 h-80 rounded-full bg-gradient-to-r from-primary/20 to-blue-400/20 blur-3xl"
style={{
transform: `translate(${scrollY * 0.1}px, ${scrollY * -0.05}px)`,
opacity: Math.max(0.2, 1 - scrollY * 0.001),
}}
/>
<div
className="absolute bottom-1/3 -right-20 w-80 h-80 rounded-full bg-gradient-to-r from-blue-400/20 to-primary/20 blur-3xl"
style={{
transform: `translate(${scrollY * -0.1}px, ${scrollY * 0.05}px)`,
opacity: Math.max(0.2, 1 - scrollY * 0.001),
}}
/>
<div className="absolute inset-0 bg-[linear-gradient(rgba(59,130,246,0.05)_1px,transparent_1px),linear-gradient(to_right,rgba(59,130,246,0.05)_1px,transparent_1px)] bg-[size:40px_40px]" />
</div>
<div className="container px-4 md:px-6 relative">
<div className="grid gap-6 lg:grid-cols-2 lg:gap-12 items-center">
<motion.div
className="flex flex-col justify-center space-y-8"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<div className="inline-flex items-center rounded-full border px-3 py-1 text-sm w-fit">
<SparklesIcon className="mr-1 h-3.5 w-3.5 text-primary" />
<span>{badgeText}</span>
</div>
<div className="space-y-4">
<h1 className="text-4xl font-bold tracking-tighter sm:text-5xl md:text-6xl lg:text-7xl bg-clip-text text-transparent bg-gradient-to-r from-foreground via-foreground to-foreground/70">
{title} <br />
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
{highlight}
</span>
</h1>
<p className="max-w-[600px] text-muted-foreground md:text-xl">
{description}
</p>
</div>
<div className="flex flex-col sm:flex-row gap-4">
<Button
size="lg"
className="bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90 transition-all duration-300 font-bold text-black hover:text-white cursor-pointer"
onClick={() => window.open(buttons.primaryUrl, '_blank')}
>
{buttons.primaryLabel}
</Button>
<Button
size="lg"
variant="outline"
className="group relative overflow-hidden border-primary"
onClick={() => window.open(buttons.secondaryUrl, '_blank')}
>
<span className="absolute inset-0 bg-gradient-to-r from-primary/10 to-blue-400/10 translate-y-full group-hover:translate-y-0 transition-transform duration-300" />
<span className="relative">{buttons.secondaryLabel}</span>
</Button>
</div>
<div className="flex items-center gap-8">
{stats?.map(({ value, label }, i) => (
<Fragment key={i}>
<div className="flex flex-col items-center">
<div className="text-2xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
{value}
</div>
<div className="text-xs text-muted-foreground">{label}</div>
</div>
{i < stats.length - 1 && (
<div className="h-10 border-r border-border mx-4" />
)}
</Fragment>
))}
</div>
</motion.div>
<motion.div
className="relative mx-auto lg:ml-auto"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<div className="relative">
<div className="absolute -top-6 -left-6 w-12 h-12 rounded-lg border border-primary/30 bg-background/50 backdrop-blur-sm flex items-center justify-center">
<CodeIcon className="h-6 w-6 text-primary" />
</div>
<div className="absolute -bottom-6 -right-6 w-12 h-12 rounded-lg border border-blue-400/30 bg-background/50 backdrop-blur-sm flex items-center justify-center">
<UsersIcon className="h-6 w-6 text-blue-400" />
</div>
<div className="relative z-10 rounded-2xl overflow-hidden border shadow-2xl">
<div className="absolute inset-0 bg-gradient-to-tr from-primary/10 to-blue-400/10" />
{(() => {
const media = formatCMSImageDataToMedia(heroImage);
if (!media) return null;
return (
<Image
src={media.url!}
alt={media.alt}
width={600}
height={500}
className="w-full h-auto object-cover"
/>
);
})()}
</div>
</div>
</motion.div>
</div>
</div>
</section>
);
}
@@ -0,0 +1,144 @@
'use client';
import { Icon } from '@iconify/react';
import { Button } from '@imphnen-frontend-service/shadcn-ui/atoms';
import { LearningResourcesSection } from 'apps/landing/src/payload-types';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
export function LearningResources(props: LearningResourcesSection) {
const { resources, featured, title, description } = props;
const ref = useRef(null);
const isInView = useInView(ref, { once: true, amount: 0.2 });
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: { duration: 0.5 },
},
};
return (
<section
id="sumber-belajar"
className="w-full py-20 md:py-32 relative overflow-hidden"
>
<div className="absolute inset-0 -z-10">
<div className="absolute inset-0 bg-gradient-to-b from-background via-background to-background" />
<div className="absolute top-0 right-0 w-1/2 h-1/2 bg-gradient-to-bl from-primary/5 to-transparent blur-3xl" />
<div className="absolute bottom-0 left-0 w-1/2 h-1/2 bg-gradient-to-tr from-blue-400/5 to-transparent blur-3xl" />
</div>
<div className="container px-4 md:px-6" ref={ref}>
<div className="flex flex-col items-center justify-center space-y-4 text-center mb-16">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5 }}
>
<h2 className="text-3xl font-bold tracking-tighter md:text-4xl/tight lg:text-5xl">
{title}
</h2>
<p className="max-w-[800px] mx-auto text-muted-foreground md:text-lg">
{description}
</p>
</motion.div>
</div>
<motion.div
className="grid gap-8 md:grid-cols-2 lg:grid-cols-4"
variants={containerVariants}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
>
{resources.map((r, i) => (
<motion.div
key={i}
className="group relative overflow-hidden rounded-xl border bg-background p-6 hover:shadow-lg"
variants={itemVariants}
>
<div className="absolute top-0 left-0 h-1 w-full bg-gradient-to-r from-primary/50 to-blue-400/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="relative z-10">
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900">
<Icon
icon={r.icon}
className="h-6 w-6 text-blue-600 dark:text-blue-400"
/>
</div>
<h3 className="mb-2 text-xl font-bold">{r.title}</h3>
<p className="mb-6 text-muted-foreground">{r.description}</p>
<Button
variant="link"
className="p-0 h-auto font-medium text-primary hover:text-primary/80"
onClick={() => window.open(r.buttonLink, '_blank')}
>
{r.buttonText}
<Icon icon="tabler:arrow-right" className="h-6 w-6" />
</Button>
</div>
<div className="absolute -bottom-32 -right-32 w-64 h-64 bg-gradient-to-tl from-primary/10 to-transparent rounded-full opacity-0 group-hover:opacity-100 transition-all duration-500 group-hover:-translate-y-10 group-hover:-translate-x-10" />
</motion.div>
))}
</motion.div>
<motion.div
className="mt-20 rounded-xl overflow-hidden border bg-background/50 backdrop-blur-sm"
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.3 }}
>
<div className="grid md:grid-cols-2 gap-0">
<div className="p-8 md:p-12 flex flex-col justify-center">
<div className="inline-block rounded-full bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary mb-4 w-fit">
{featured.label}
</div>
<h3 className="text-2xl md:text-3xl font-bold mb-4">
{featured.title}
</h3>
<p className="text-muted-foreground mb-6">
{featured.description}
</p>
<div className="flex flex-wrap gap-4">
<Button
className="bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90 font-bold text-black hover:text-white cursor-pointer"
onClick={() =>
window.open(featured.primaryButtonLink, '_blank')
}
>
{featured.primaryButtonText}
</Button>
<Button
variant="outline"
onClick={() =>
window.open(featured.secondaryButtonLink, '_blank')
}
>
{featured.secondaryButtonText}
</Button>
</div>
</div>
<div className="relative h-64 md:h-auto">
<div className="absolute inset-0 bg-gradient-to-br from-primary/20 to-blue-400/20" />
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-16 h-16 rounded-full bg-background/80 backdrop-blur-sm flex items-center justify-center cursor-pointer hover:bg-background transition-colors">
<div className="w-0 h-0 border-t-8 border-t-transparent border-l-12 border-l-primary border-b-8 border-b-transparent ml-1" />
</div>
</div>
</div>
</div>
</motion.div>
</div>
</section>
);
}
@@ -0,0 +1,42 @@
'use client';
import {
Button,
MoonIcon,
SunIcon,
} from '@imphnen-frontend-service/shadcn-ui/atoms';
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';
export function SimpleThemeToggle() {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
const toggleTheme = () => {
setTheme(theme === 'dark' ? 'light' : 'dark');
};
return (
<Button
variant="outline"
size="icon"
onClick={toggleTheme}
className="focus-visible:ring-0 cursor-pointer"
>
{theme === 'dark' ? (
<SunIcon className="h-[1.2rem] w-[1.2rem]" />
) : (
<MoonIcon className="h-[1.2rem] w-[1.2rem]" />
)}
<span className="sr-only">Toggle theme</span>
</Button>
);
}
@@ -0,0 +1,137 @@
'use client';
import { QuoteIcon } from '@imphnen-frontend-service/shadcn-ui/atoms';
import { formatCMSImageDataToMedia } from 'apps/landing/src/lib/format';
import { TestimonialsSection } from 'apps/landing/src/payload-types';
import { motion, useInView } from 'framer-motion';
import Image from 'next/image';
import { useRef } from 'react';
export function Testimonials(props: TestimonialsSection) {
const { title, subtitle, items, stats, joinTitle, joinText } = props;
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, amount: 0.2 });
const containerVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1, transition: { staggerChildren: 0.1 } },
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: { y: 0, opacity: 1, transition: { duration: 0.5 } },
};
return (
<section
id="testimoni"
className="w-full py-20 md:py-32 bg-muted relative overflow-hidden"
ref={ref}
>
<div className="absolute inset-0 -z-10">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,rgba(59,130,246,0.1),transparent_50%)]" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom,rgba(96,165,250,0.1),transparent_50%)]" />
</div>
<div className="container px-4 md:px-6">
<motion.div
className="flex flex-col items-center justify-center space-y-4 text-center mb-16"
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5 }}
>
<h2 className="text-3xl font-bold tracking-tighter md:text-4xl lg:text-5xl">
{title.split(' ').map((word, i) =>
i === title.split(' ').length - 1 ? (
<span
key={i}
className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400"
>
{word}
</span>
) : (
<span key={i}>{word} </span>
)
)}
</h2>
<p className="max-w-[800px] mx-auto text-muted-foreground md:text-lg">
{subtitle}
</p>
</motion.div>
<motion.div
className="grid gap-8 md:grid-cols-3"
variants={containerVariants}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
>
{items?.map((t, idx) => (
<motion.div
key={idx}
className="group relative overflow-hidden rounded-xl border bg-background p-6 transition-all hover:shadow-lg"
variants={itemVariants}
>
<div className="absolute top-6 right-6 text-primary/20 group-hover:text-primary/40 transition-colors">
<QuoteIcon className="h-8 w-8" />
</div>
<div className="relative z-10">
<p className="mb-6 text-muted-foreground italic">
&ldquo;{t.quote}&rdquo;
</p>
<div className="flex items-center gap-4">
<div className="relative h-12 w-12 overflow-hidden rounded-full border-2 border-primary/20">
{(() => {
const media = formatCMSImageDataToMedia(t.avatar);
if (!media) return null;
return (
<Image
src={media.url!}
alt={media.alt}
width={600}
height={500}
className="object-cover"
/>
);
})()}
</div>
<div>
<h4 className="font-bold">{t.name}</h4>
<p className="text-sm text-muted-foreground">{t.role}</p>
</div>
</div>
</div>
<div className="absolute -bottom-1 -left-1 w-20 h-20 bg-gradient-to-tr from-primary/10 to-transparent rounded-tr-full opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
</motion.div>
))}
</motion.div>
<motion.div
className="mt-20 rounded-xl overflow-hidden border bg-background/50 backdrop-blur-sm p-8 md:p-12"
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.3 }}
>
<div className="grid md:grid-cols-2 gap-8 items-center">
<div>
<h3 className="text-2xl md:text-3xl font-bold mb-4">
{joinTitle}
</h3>
<p className="text-muted-foreground">{joinText}</p>
</div>
<div className="grid grid-cols-2 gap-4">
{stats?.map((s, idx) => (
<div key={idx} className="rounded-lg border p-4 text-center">
<div className="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
{s.value}
</div>
<div className="text-sm text-muted-foreground">{s.label}</div>
</div>
))}
</div>
</div>
</motion.div>
</div>
</section>
);
}
@@ -0,0 +1,8 @@
'use client';
import type { ThemeProviderProps } from 'next-themes';
import { ThemeProvider as NextThemesProvider } from 'next-themes';
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
@@ -0,0 +1,38 @@
import { type Metadata } from 'next';
import { Inter } from 'next/font/google';
import '../../styles/globals.css';
import Footer from './_components/footer';
import { Header } from './_components/header';
import { ThemeProvider } from './_components/theme-provider';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'IMPHNEN - Ingin Menjadi Programmer Handal?',
description: 'Komunitas belajar programming untuk semua level',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="id" suppressHydrationWarning>
<body className={inter.className}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<div className="flex min-h-screen flex-col">
<Header />
<main className="flex-1">{children}</main>
<Footer />
</div>
</ThemeProvider>
</body>
</html>
);
}
+43
View File
@@ -0,0 +1,43 @@
import { getGlobalsCallToActionSection } from '../../services/get-globals-call-to-action-section';
import { getGlobalsCommunitiesSection } from '../../services/get-globals-communitues-section';
import { getGlobalsFeaturesSection } from '../../services/get-globals-features-section';
import { getGlobalsHeroSection } from '../../services/get-globals-hero-section';
import { getGlobalsLearningResourcesSection } from '../../services/get-globals-learning-resources-section';
import { getGlobalsTestimonialsSection } from '../../services/get-globals-testimonials-section';
import { CallToAction } from './_components/call-to-action';
import { Community } from './_components/community';
import { Features } from './_components/features';
import { Hero } from './_components/hero';
import { LearningResources } from './_components/learning-resources';
import { Testimonials } from './_components/testimonials';
export const revalidate = 10; // Seconds
export default async function Page() {
const [
heroData,
featuresData,
communitiesData,
learningResourcesData,
testimonialsData,
callToActionData,
] = await Promise.all([
getGlobalsHeroSection(),
getGlobalsFeaturesSection(),
getGlobalsCommunitiesSection(),
getGlobalsLearningResourcesSection(),
getGlobalsTestimonialsSection(),
getGlobalsCallToActionSection(),
]);
return (
<>
<Hero {...heroData} />
<Features {...featuresData} />
<Community {...communitiesData} />
<LearningResources {...learningResourcesData} />
<Testimonials {...testimonialsData} />
<CallToAction {...callToActionData} />
</>
);
}
@@ -0,0 +1,27 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from 'next';
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views';
import config from '../../../../payload.config';
import { importMap } from '../importMap';
type Args = {
params: Promise<{
segments: string[];
}>;
searchParams: Promise<{
[key: string]: string | string[];
}>;
};
export const generateMetadata = ({
params,
searchParams,
}: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams });
const NotFound = ({ params, searchParams }: Args) =>
NotFoundPage({ config, params, searchParams, importMap });
export default NotFound;
@@ -0,0 +1,27 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from 'next';
import { RootPage, generatePageMetadata } from '@payloadcms/next/views';
import config from '../../../../payload.config';
import { importMap } from '../importMap';
type Args = {
params: Promise<{
segments: string[];
}>;
searchParams: Promise<{
[key: string]: string | string[];
}>;
};
export const generateMetadata = ({
params,
searchParams,
}: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams });
const Page = ({ params, searchParams }: Args) =>
RootPage({ config, params, searchParams, importMap });
export default Page;
@@ -0,0 +1,5 @@
import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client'
export const importMap = {
"@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24
}
@@ -0,0 +1,19 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import '@payloadcms/next/css';
import {
REST_DELETE,
REST_GET,
REST_OPTIONS,
REST_PATCH,
REST_POST,
REST_PUT,
} from '@payloadcms/next/routes';
import config from '../../../../payload.config';
export const GET = REST_GET(config);
export const POST = REST_POST(config);
export const DELETE = REST_DELETE(config);
export const PATCH = REST_PATCH(config);
export const PUT = REST_PUT(config);
export const OPTIONS = REST_OPTIONS(config);
@@ -0,0 +1,7 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import '@payloadcms/next/css';
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes';
import config from '../../../../payload.config';
export const GET = GRAPHQL_PLAYGROUND_GET(config);
@@ -0,0 +1,8 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes';
import config from '../../../../payload.config';
export const POST = GRAPHQL_POST(config);
export const OPTIONS = REST_OPTIONS(config);
+35
View File
@@ -0,0 +1,35 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import '@payloadcms/next/css';
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts';
import type { ServerFunctionClient } from 'payload';
import React from 'react';
import config from '../../payload.config';
import { importMap } from './admin/importMap.js';
import './custom.scss';
type Args = {
children: React.ReactNode;
};
const serverFunction: ServerFunctionClient = async function (args) {
'use server';
return handleServerFunctions({
...args,
config,
importMap,
});
};
const Layout = ({ children }: Args) => (
<RootLayout
config={config}
importMap={importMap}
serverFunction={serverFunction}
>
{children}
</RootLayout>
);
export default Layout;
@@ -0,0 +1,51 @@
import { GlobalConfig } from 'payload';
export const CallToActionSection: GlobalConfig = {
slug: 'call-to-action-section',
label: 'Call To Action Section',
fields: [
{
name: 'title',
type: 'text',
required: true,
defaultValue: 'Siap Menjadi',
},
{
name: 'highlightedTitle',
type: 'text',
required: true,
defaultValue: 'Programmer Handal?',
},
{
name: 'subtitle',
type: 'textarea',
required: true,
defaultValue:
'Bergabunglah dengan komunitas IMPHNEN sekarang dan mulai perjalanan programming mu dengan cara yang menyenangkan!',
},
{
name: 'primaryButtonLabel',
type: 'text',
required: true,
defaultValue: 'Gabung Discord',
},
{
name: 'primaryButtonLink',
type: 'text',
required: true,
defaultValue: 'https://discord.gg/imphnen',
},
{
name: 'secondaryButtonLabel',
type: 'text',
required: true,
defaultValue: 'Join Facebook Group',
},
{
name: 'secondaryButtonLink',
type: 'text',
required: true,
defaultValue: 'https://facebook.com/groups/programmerhandal',
},
],
};
@@ -0,0 +1,65 @@
import { GlobalConfig } from 'payload';
export const CommunitiesSection: GlobalConfig = {
slug: 'communities-section',
label: 'Communities Section',
fields: [
{
name: 'items',
type: 'array',
required: true,
defaultValue: [
{
iconName: 'tabler:brand-facebook',
title: 'Facebook Group',
description:
'Bergabunglah dengan grup Facebook kami untuk diskusi santai dan berbagi artikel menarik.',
buttonText: 'Gabung Sekarang',
buttonLink: 'https://facebook.com/groups/programmerhandal',
},
{
iconName: 'tabler:brand-instagram',
title: 'Instagram',
description:
'Ikuti kami di Instagram untuk tips programming, konten inspiratif, dan info event terbaru.',
buttonText: 'Follow Kami',
buttonLink: 'https://www.instagram.com/imphnen.dev',
},
{
iconName: 'tabler:brand-discord-filled',
title: 'Discord Server',
description:
'Diskusikan langsung dengan sesama programmer dan dapatkan bantuan langsung dari para ahli.',
buttonText: 'Join Server',
buttonLink: 'https://discord.com/invite/imphnen',
},
],
fields: [
{
name: 'iconName',
type: 'text',
required: true,
},
{ name: 'title', type: 'text', required: true },
{ name: 'description', type: 'textarea', required: true },
{ name: 'buttonText', type: 'text', required: true },
{ name: 'buttonLink', type: 'text', required: true },
],
},
{
name: 'stats',
type: 'array',
required: true,
defaultValue: [
{ value: '100K+', label: 'Member Aktif' },
{ value: '50+', label: 'Event Bulanan' },
{ value: '100+', label: 'Mentor Profesional' },
{ value: '5K+', label: 'Diskusi Mingguan' },
],
fields: [
{ name: 'value', type: 'text', required: true },
{ name: 'label', type: 'text', required: true },
],
},
],
};
@@ -0,0 +1,65 @@
import { GlobalConfig } from 'payload';
export const FeaturesSection: GlobalConfig = {
slug: 'features-section',
label: 'Features Section',
fields: [
{
name: 'heading',
type: 'text',
defaultValue: 'Belajar programming dengan cara yang lebih baik',
},
{
name: 'subheading',
type: 'text',
defaultValue:
'IMPHNEN hadir dengan berbagai fitur untuk membantu kamu menjadi programmer handal tanpa harus pusing dengan coding.',
},
{
name: 'features',
type: 'array',
defaultValue: [
{
iconName: 'tabler:device-laptop',
title: 'Belajar Tanpa Koding',
description:
'Pelajari konsep programming dengan cara yang mudah dipahami tanpa harus menulis kode yang rumit.',
},
{
iconName: 'tabler:users',
title: 'Komunitas Supportif',
description:
'Bergabunglah dengan komunitas programmer Indonesia yang siap membantu dan berbagi pengalaman.',
},
{
iconName: 'tabler:book',
title: 'Tutorial Interaktif',
description:
'Akses tutorial interaktif yang membuat konsep programming lebih mudah untuk dipahami.',
},
{
iconName: 'tabler:code',
title: 'Proyek Praktis',
description:
'Terapkan pengetahuan Anda dalam proyek nyata dengan panduan langkah demi langkah.',
},
],
fields: [
{
name: 'iconName',
type: 'text',
required: true,
},
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'description',
type: 'textarea',
},
],
},
],
};
+104
View File
@@ -0,0 +1,104 @@
import type { GlobalConfig } from 'payload';
export const HeroSection: GlobalConfig = {
slug: 'hero-section',
label: 'Hero Section',
fields: [
{
name: 'heroImage',
label: 'Hero Image',
type: 'upload',
relationTo: 'media',
required: true,
},
{
name: 'badgeText',
label: 'Badge Text',
type: 'text',
required: true,
defaultValue: 'Komunitas Programmer Indonesia',
},
{
name: 'title',
label: 'Main Title',
type: 'text',
required: true,
defaultValue: 'Programmer Handal,',
},
{
name: 'highlight',
label: 'Highlight Text',
type: 'text',
required: true,
defaultValue: 'Tanpa Ribet',
},
{
name: 'description',
label: 'Description',
type: 'textarea',
required: true,
defaultValue:
'Temukan potensi programming Anda bersama komunitas yang mendukung, tutorial interaktif, dan sumber daya berkualitas tinggi.',
},
{
type: 'group',
name: 'buttons',
label: 'Buttons',
fields: [
{
name: 'primaryLabel',
label: 'Primary Button Label',
type: 'text',
required: true,
defaultValue: 'Mulai Belajar',
},
{
name: 'primaryUrl',
label: 'Primary Button URL',
type: 'text',
required: true,
defaultValue: 'https://facebook.com/groups/programmerhandal',
},
{
name: 'secondaryLabel',
label: 'Secondary Button Label',
type: 'text',
required: true,
defaultValue: 'Gabung Discord',
},
{
name: 'secondaryUrl',
label: 'Secondary Button URL',
type: 'text',
required: true,
defaultValue: 'https://discord.com/invite/imphnen',
},
],
},
{
name: 'stats',
label: 'Statistics',
type: 'array',
minRows: 1,
fields: [
{
name: 'value',
label: 'Value',
type: 'text',
required: true,
},
{
name: 'label',
label: 'Label',
type: 'text',
required: true,
},
],
defaultValue: [
{ value: '180K+', label: 'Member' },
{ value: '500+', label: 'Tutorial' },
{ value: '24/7', label: 'Yapping' },
],
},
],
};
@@ -0,0 +1,117 @@
// payload config
import { GlobalConfig } from 'payload';
export const LearningResourcesSection: GlobalConfig = {
slug: 'learning-resources-section',
label: 'Learning Resources Section',
fields: [
{
name: 'title',
type: 'text',
required: true,
defaultValue: 'Sumber Belajar',
},
{
name: 'description',
type: 'textarea',
required: true,
defaultValue:
'Akses berbagai materi belajar yang akan membantu kamu menguasai konsep programming dengan cara yang menyenangkan.',
},
{
name: 'resources',
type: 'array',
required: true,
fields: [
{ name: 'icon', type: 'text', required: true },
{ name: 'title', type: 'text', required: true },
{ name: 'description', type: 'textarea', required: true },
{ name: 'buttonText', type: 'text', required: true },
{ name: 'buttonLink', type: 'text', required: true },
],
defaultValue: [
{
icon: 'tabler:video',
title: 'Video Tutorial',
description:
'Belajar melalui tutorial video dari langkah awal hingga mahir.',
buttonText: 'Lihat Semua Video',
buttonLink: '#',
},
{
icon: 'tabler:article',
title: 'Artikel & Tutorial',
description:
'Pelajari konsep programming melalui artikel yang disusun secara terstruktur.',
buttonText: 'Baca Artikel',
buttonLink: '#',
},
{
icon: 'tabler:brand-vscode',
title: 'Tantangan Koding',
description:
'Uji kemampuan koding kamu dengan tantangan yang menyenangkan dan menantang.',
buttonText: 'Mulai Tantangan',
buttonLink: '#',
},
{
icon: 'tabler:device-desktop-share',
title: 'Sharing Session',
description:
'Ikuti sesi berbagi pengalaman dari programmer berpengalaman dan belajar dari pengalaman mereka.',
buttonText: 'Jadwal Session',
buttonLink: '#',
},
],
},
{
name: 'featured',
type: 'group',
fields: [
{
name: 'label',
type: 'text',
required: true,
defaultValue: 'Rekomendasi Terbaik',
},
{
name: 'title',
type: 'text',
required: true,
defaultValue: 'Kursus Lengkap Web Development',
},
{
name: 'description',
type: 'textarea',
required: true,
defaultValue:
'Pelajari HTML, CSS, JavaScript, React, dan Node.js dalam satu kursus komprehensif yang dirancang untuk pemula hingga tingkat menengah.',
},
{
name: 'primaryButtonText',
type: 'text',
required: true,
defaultValue: 'Mulai Kursus',
},
{
name: 'primaryButtonLink',
type: 'text',
required: true,
defaultValue: '#',
},
{
name: 'secondaryButtonText',
type: 'text',
required: true,
defaultValue: 'Lihat Silabus',
},
{
name: 'secondaryButtonLink',
type: 'text',
required: true,
defaultValue: '#',
},
],
},
],
};
+16
View File
@@ -0,0 +1,16 @@
import type { CollectionConfig } from 'payload';
export const Media: CollectionConfig = {
slug: 'media',
access: {
read: () => true,
},
fields: [
{
name: 'alt',
type: 'text',
required: true,
},
],
upload: true,
};
@@ -0,0 +1,93 @@
import { GlobalConfig } from 'payload';
export const TestimonialsSection: GlobalConfig = {
slug: 'testimonials-section',
label: 'Testimonials Section',
fields: [
{
name: 'title',
label: 'Section Title',
type: 'text',
defaultValue: 'Testimoni Member',
required: true,
},
{
name: 'subtitle',
label: 'Section Subtitle',
type: 'textarea',
defaultValue:
'Apa kata mereka yang telah bergabung dengan komunitas IMPHNEN?',
required: true,
},
{
name: 'items',
label: 'Testimonials Items',
type: 'array',
fields: [
{ name: 'quote', type: 'textarea', label: 'Quote', required: true },
{ name: 'name', type: 'text', label: 'Name', required: true },
{ name: 'role', type: 'text', label: 'Role', required: true },
{
name: 'avatar',
type: 'upload',
label: 'Avatar',
relationTo: 'media',
required: true,
},
],
defaultValue: [
{
quote:
'Saya yang tadinya tidak mengerti apa-apa tentang programming, sekarang bisa membuat website sendiri dengan mudah. Terima kasih IMPHNEN!',
name: 'Budi Santoso',
role: 'Web Developer Pemula',
avatar: { id: '', relationTo: 'media' },
},
{
quote:
'Komunitas yang sangat supportif! Setiap pertanyaan selalu dijawab dengan cepat dan jelas. Diskusi programnya mudah dipahami.',
name: 'Anita Ratna',
role: 'Mobile App Developer',
avatar: { id: '', relationTo: 'media' },
},
{
quote:
'Server Discord IMPHNEN adalah tempat belajar terbaik untuk programmer pemula seperti saya. Materinya lengkap dan komunitasnya sangat membantu!',
name: 'Dedi Permana',
role: 'Data Scientist',
avatar: { id: '', relationTo: 'media' },
},
],
},
{
name: 'stats',
label: 'Statistics',
type: 'array',
fields: [
{ name: 'value', type: 'text', label: 'Value', required: true },
{ name: 'label', type: 'text', label: 'Label', required: true },
],
defaultValue: [
{ value: '98%', label: 'Tingkat Kemalasan' },
{ value: '4.9/5', label: 'Rating Drama' },
{ value: '85%', label: 'Mendapat Pekerjaan' },
{ value: '24/7', label: 'Yapping' },
],
},
{
name: 'joinTitle',
label: 'Join Section Title',
type: 'text',
defaultValue: 'Bergabunglah dengan 10,000+ programmer Indonesia lainnya',
required: true,
},
{
name: 'joinText',
label: 'Join Section Text',
type: 'textarea',
defaultValue:
'Komunitas kami terus berkembang dengan programmer dari berbagai latar belakang dan tingkat keahlian. Bersama-sama, kita belajar, berbagi, dan tumbuh sebagai profesional.',
required: true,
},
],
};
+13
View File
@@ -0,0 +1,13 @@
import type { CollectionConfig } from 'payload';
export const Users: CollectionConfig = {
slug: 'users',
admin: {
useAsTitle: 'email',
},
auth: true,
fields: [
// Email added by default
// Add more fields as needed
],
};
+10
View File
@@ -0,0 +1,10 @@
import { Media } from '../payload-types';
export function formatCMSImageDataToMedia(
image: number | Media | null | undefined
): Media | null {
if (image && typeof image === 'object' && 'url' in image) {
return image as Media;
}
return null;
}
+4
View File
@@ -0,0 +1,4 @@
import { getPayload } from 'payload';
import config from '../payload.config';
export const payload = await getPayload({ config });
+591
View File
@@ -0,0 +1,591 @@
/* tslint:disable */
/* eslint-disable */
/**
* This file was automatically generated by Payload.
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
* and re-run `payload generate:types` to regenerate this file.
*/
/**
* Supported timezones in IANA format.
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "supportedTimezones".
*/
export type SupportedTimezones =
| 'Pacific/Midway'
| 'Pacific/Niue'
| 'Pacific/Honolulu'
| 'Pacific/Rarotonga'
| 'America/Anchorage'
| 'Pacific/Gambier'
| 'America/Los_Angeles'
| 'America/Tijuana'
| 'America/Denver'
| 'America/Phoenix'
| 'America/Chicago'
| 'America/Guatemala'
| 'America/New_York'
| 'America/Bogota'
| 'America/Caracas'
| 'America/Santiago'
| 'America/Buenos_Aires'
| 'America/Sao_Paulo'
| 'Atlantic/South_Georgia'
| 'Atlantic/Azores'
| 'Atlantic/Cape_Verde'
| 'Europe/London'
| 'Europe/Berlin'
| 'Africa/Lagos'
| 'Europe/Athens'
| 'Africa/Cairo'
| 'Europe/Moscow'
| 'Asia/Riyadh'
| 'Asia/Dubai'
| 'Asia/Baku'
| 'Asia/Karachi'
| 'Asia/Tashkent'
| 'Asia/Calcutta'
| 'Asia/Dhaka'
| 'Asia/Almaty'
| 'Asia/Jakarta'
| 'Asia/Bangkok'
| 'Asia/Shanghai'
| 'Asia/Singapore'
| 'Asia/Tokyo'
| 'Asia/Seoul'
| 'Australia/Brisbane'
| 'Australia/Sydney'
| 'Pacific/Guam'
| 'Pacific/Noumea'
| 'Pacific/Auckland'
| 'Pacific/Fiji';
export interface Config {
auth: {
users: UserAuthOperations;
};
blocks: {};
collections: {
users: User;
media: Media;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
collectionsJoins: {};
collectionsSelect: {
users: UsersSelect<false> | UsersSelect<true>;
media: MediaSelect<false> | MediaSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: number;
};
globals: {
'hero-section': HeroSection;
'features-section': FeaturesSection;
'communities-section': CommunitiesSection;
'learning-resources-section': LearningResourcesSection;
'testimonials-section': TestimonialsSection;
'call-to-action-section': CallToActionSection;
};
globalsSelect: {
'hero-section': HeroSectionSelect<false> | HeroSectionSelect<true>;
'features-section': FeaturesSectionSelect<false> | FeaturesSectionSelect<true>;
'communities-section': CommunitiesSectionSelect<false> | CommunitiesSectionSelect<true>;
'learning-resources-section': LearningResourcesSectionSelect<false> | LearningResourcesSectionSelect<true>;
'testimonials-section': TestimonialsSectionSelect<false> | TestimonialsSectionSelect<true>;
'call-to-action-section': CallToActionSectionSelect<false> | CallToActionSectionSelect<true>;
};
locale: null;
user: User & {
collection: 'users';
};
jobs: {
tasks: unknown;
workflows: unknown;
};
}
export interface UserAuthOperations {
forgotPassword: {
email: string;
password: string;
};
login: {
email: string;
password: string;
};
registerFirstUser: {
email: string;
password: string;
};
unlock: {
email: string;
password: string;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
*/
export interface User {
id: number;
updatedAt: string;
createdAt: string;
email: string;
resetPasswordToken?: string | null;
resetPasswordExpiration?: string | null;
salt?: string | null;
hash?: string | null;
loginAttempts?: number | null;
lockUntil?: string | null;
password?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "media".
*/
export interface Media {
id: number;
alt: string;
updatedAt: string;
createdAt: string;
url?: string | null;
thumbnailURL?: string | null;
filename?: string | null;
mimeType?: string | null;
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: number;
document?:
| ({
relationTo: 'users';
value: number | User;
} | null)
| ({
relationTo: 'media';
value: number | Media;
} | null);
globalSlug?: string | null;
user: {
relationTo: 'users';
value: number | User;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: number;
user: {
relationTo: 'users';
value: number | User;
};
key?: string | null;
value?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: number;
name?: string | null;
batch?: number | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".
*/
export interface UsersSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
email?: T;
resetPasswordToken?: T;
resetPasswordExpiration?: T;
salt?: T;
hash?: T;
loginAttempts?: T;
lockUntil?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "media_select".
*/
export interface MediaSelect<T extends boolean = true> {
alt?: T;
updatedAt?: T;
createdAt?: T;
url?: T;
thumbnailURL?: T;
filename?: T;
mimeType?: T;
filesize?: T;
width?: T;
height?: T;
focalX?: T;
focalY?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".
*/
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
document?: T;
globalSlug?: T;
user?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences_select".
*/
export interface PayloadPreferencesSelect<T extends boolean = true> {
user?: T;
key?: T;
value?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations_select".
*/
export interface PayloadMigrationsSelect<T extends boolean = true> {
name?: T;
batch?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "hero-section".
*/
export interface HeroSection {
id: number;
heroImage: number | Media;
badgeText: string;
title: string;
highlight: string;
description: string;
buttons: {
primaryLabel: string;
primaryUrl: string;
secondaryLabel: string;
secondaryUrl: string;
};
stats?:
| {
value: string;
label: string;
id?: string | null;
}[]
| null;
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "features-section".
*/
export interface FeaturesSection {
id: number;
heading?: string | null;
subheading?: string | null;
features?:
| {
iconName: string;
title: string;
description?: string | null;
id?: string | null;
}[]
| null;
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "communities-section".
*/
export interface CommunitiesSection {
id: number;
items: {
iconName: string;
title: string;
description: string;
buttonText: string;
buttonLink: string;
id?: string | null;
}[];
stats: {
value: string;
label: string;
id?: string | null;
}[];
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "learning-resources-section".
*/
export interface LearningResourcesSection {
id: number;
title: string;
description: string;
resources: {
icon: string;
title: string;
description: string;
buttonText: string;
buttonLink: string;
id?: string | null;
}[];
featured: {
label: string;
title: string;
description: string;
primaryButtonText: string;
primaryButtonLink: string;
secondaryButtonText: string;
secondaryButtonLink: string;
};
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "testimonials-section".
*/
export interface TestimonialsSection {
id: number;
title: string;
subtitle: string;
items?:
| {
quote: string;
name: string;
role: string;
avatar: number | Media;
id?: string | null;
}[]
| null;
stats?:
| {
value: string;
label: string;
id?: string | null;
}[]
| null;
joinTitle: string;
joinText: string;
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "call-to-action-section".
*/
export interface CallToActionSection {
id: number;
title: string;
highlightedTitle: string;
subtitle: string;
primaryButtonLabel: string;
primaryButtonLink: string;
secondaryButtonLabel: string;
secondaryButtonLink: string;
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "hero-section_select".
*/
export interface HeroSectionSelect<T extends boolean = true> {
heroImage?: T;
badgeText?: T;
title?: T;
highlight?: T;
description?: T;
buttons?:
| T
| {
primaryLabel?: T;
primaryUrl?: T;
secondaryLabel?: T;
secondaryUrl?: T;
};
stats?:
| T
| {
value?: T;
label?: T;
id?: T;
};
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "features-section_select".
*/
export interface FeaturesSectionSelect<T extends boolean = true> {
heading?: T;
subheading?: T;
features?:
| T
| {
iconName?: T;
title?: T;
description?: T;
id?: T;
};
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "communities-section_select".
*/
export interface CommunitiesSectionSelect<T extends boolean = true> {
items?:
| T
| {
iconName?: T;
title?: T;
description?: T;
buttonText?: T;
buttonLink?: T;
id?: T;
};
stats?:
| T
| {
value?: T;
label?: T;
id?: T;
};
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "learning-resources-section_select".
*/
export interface LearningResourcesSectionSelect<T extends boolean = true> {
title?: T;
description?: T;
resources?:
| T
| {
icon?: T;
title?: T;
description?: T;
buttonText?: T;
buttonLink?: T;
id?: T;
};
featured?:
| T
| {
label?: T;
title?: T;
description?: T;
primaryButtonText?: T;
primaryButtonLink?: T;
secondaryButtonText?: T;
secondaryButtonLink?: T;
};
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "testimonials-section_select".
*/
export interface TestimonialsSectionSelect<T extends boolean = true> {
title?: T;
subtitle?: T;
items?:
| T
| {
quote?: T;
name?: T;
role?: T;
avatar?: T;
id?: T;
};
stats?:
| T
| {
value?: T;
label?: T;
id?: T;
};
joinTitle?: T;
joinText?: T;
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "call-to-action-section_select".
*/
export interface CallToActionSectionSelect<T extends boolean = true> {
title?: T;
highlightedTitle?: T;
subtitle?: T;
primaryButtonLabel?: T;
primaryButtonLink?: T;
secondaryButtonLabel?: T;
secondaryButtonLink?: T;
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".
*/
export interface Auth {
[k: string]: unknown;
}
declare module 'payload' {
export interface GeneratedTypes extends Config {}
}
+78
View File
@@ -0,0 +1,78 @@
import { postgresAdapter } from '@payloadcms/db-postgres';
import { lexicalEditor } from '@payloadcms/richtext-lexical';
import { s3Storage } from '@payloadcms/storage-s3';
import { NodeHttpHandler } from '@smithy/node-http-handler';
import https from 'https';
import path from 'path';
import { buildConfig } from 'payload';
import sharp from 'sharp';
import { fileURLToPath } from 'url';
import { CallToActionSection } from './collections/CallToActionSection';
import { CommunitiesSection } from './collections/CommunitiesSection';
import { FeaturesSection } from './collections/FeaturesSection';
import { HeroSection } from './collections/HeroSection';
import { LearningResourcesSection } from './collections/LearningResourcesSection';
import { Media } from './collections/Media';
import { TestimonialsSection } from './collections/TestimonialsSection';
import { Users } from './collections/Users';
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
export default buildConfig({
admin: {
user: Users.slug,
importMap: {
importMapFile: path.resolve(
__dirname,
'app',
'(payload)',
'admin',
'importMap.js'
),
},
},
collections: [Users, Media],
globals: [
HeroSection,
FeaturesSection,
CommunitiesSection,
LearningResourcesSection,
TestimonialsSection,
CallToActionSection,
],
editor: lexicalEditor(),
secret: process.env.CMS_SECRET || '',
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
db: postgresAdapter({
pool: {
connectionString: process.env.CMS_POSTGRES_URL || '',
},
}),
sharp,
plugins: [
s3Storage({
collections: {
media: {
disableLocalStorage: true,
},
},
bucket: 'landing-cms',
config: {
endpoint: process.env.CMS_STORAGE_ENDPOINT,
credentials: {
accessKeyId: process.env.CMS_STORAGE_ACCESS_KEY_ID!,
secretAccessKey: process.env.CMS_STORAGE_SECRET_ACCESS_KEY!,
},
region: process.env.CMS_STORAGE_REGION!,
forcePathStyle: true,
requestHandler: new NodeHttpHandler({
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
}),
},
}),
],
});
@@ -0,0 +1,7 @@
import { payload } from '../lib/payload';
export async function getGlobalsCallToActionSection() {
return await payload.findGlobal({
slug: 'call-to-action-section',
});
}
@@ -0,0 +1,7 @@
import { payload } from '../lib/payload';
export async function getGlobalsCommunitiesSection() {
return await payload.findGlobal({
slug: 'communities-section',
});
}
@@ -0,0 +1,7 @@
import { payload } from '../lib/payload';
export async function getGlobalsFeaturesSection() {
return await payload.findGlobal({
slug: 'features-section',
});
}
@@ -0,0 +1,7 @@
import { payload } from '../lib/payload';
export async function getGlobalsHeroSection() {
return await payload.findGlobal({
slug: 'hero-section',
});
}
@@ -0,0 +1,7 @@
import { payload } from '../lib/payload';
export async function getGlobalsLearningResourcesSection() {
return await payload.findGlobal({
slug: 'learning-resources-section',
});
}
@@ -0,0 +1,7 @@
import { payload } from '../lib/payload';
export async function getGlobalsTestimonialsSection() {
return await payload.findGlobal({
slug: 'testimonials-section',
});
}
+3
View File
@@ -0,0 +1,3 @@
@import 'tailwindcss';
@import "../../../../libs/shadcn-ui/src/index.css";
@source "../../../../libs/shadcn-ui/src/atoms/**/*.{ts,tsx}";
+45
View File
@@ -0,0 +1,45 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "preserve",
"strict": true,
"noEmit": true,
"emitDeclarationOnly": false,
"esModuleInterop": true,
"target": "ES2022",
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"incremental": true,
"plugins": [
{
"name": "next"
}
]
},
"include": [
"**/*.js",
"**/*.jsx",
"**/*.ts",
"**/*.tsx",
"../../apps/landing/.next/types/**/*.ts",
"../../dist/apps/landing/.next/types/**/*.ts",
"next-env.d.ts",
".next/types/**/*.ts"
],
"exclude": [
"node_modules",
"jest.config.ts",
"**/*.spec.ts",
"**/*.test.ts"
]
}
+1 -1
View File
@@ -12,7 +12,7 @@ export default [
], ],
}, },
{ {
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'],
rules: { rules: {
'@nx/enforce-module-boundaries': [ '@nx/enforce-module-boundaries': [
'error', 'error',
+29 -27
View File
@@ -16,7 +16,7 @@ export const authLoginSchema = z.object({
.min(1, 'Password tidak boleh kosong'), .min(1, 'Password tidak boleh kosong'),
}); });
export const authRegisterSchema = z export const stepOneRegisterSchema = z
.object({ .object({
email: z email: z
.string({ .string({
@@ -40,24 +40,6 @@ export const authRegisterSchema = z
.min(1, 'Password tidak boleh kosong') .min(1, 'Password tidak boleh kosong')
.min(8, 'Password harus lebih dari 8 karakter') .min(8, 'Password harus lebih dari 8 karakter')
.max(50, 'Password tidak boleh lebih dari 50 karakter'), .max(50, 'Password tidak boleh lebih dari 50 karakter'),
phone_number: z
.string({
required_error: 'Nomor telepon tidak boleh kosong',
invalid_type_error: 'Nomor telepon harus berupa string',
})
.min(1, 'Nomor telepon tidak boleh kosong')
.max(15, 'Nomor telepon tidak boleh lebih dari 15 karakter'),
referral_code: z
.string()
.min(1, 'Kode referral tidak boleh kosong')
.max(4, 'Kode referral tidak boleh lebih dari 4 karakter')
.optional(),
referred_by: z
.string()
.min(1, 'Kode referral tidak boleh kosong')
.max(50, 'Kode referral tidak boleh lebih dari 50 karakter')
.optional(),
student_type: z.string(),
confirm_password: z confirm_password: z
.string({ .string({
required_error: 'Konfirmasi password tidak boleh kosong', required_error: 'Konfirmasi password tidak boleh kosong',
@@ -65,12 +47,32 @@ export const authRegisterSchema = z
.min(1, 'Konfirmasi password tidak boleh kosong') .min(1, 'Konfirmasi password tidak boleh kosong')
.max(50, 'Konfirmasi password tidak boleh lebih dari 50 karakter'), .max(50, 'Konfirmasi password tidak boleh lebih dari 50 karakter'),
}) })
.superRefine((val, ctx) => { .refine((data) => data.password === data.confirm_password, {
if (val.password !== val.confirm_password) { message: 'Password dan Konfirmasi Password harus sama',
ctx.addIssue({ path: ['confirm_password'],
code: z.ZodIssueCode.custom,
path: ['confirm_password'],
message: `No duplicates allowed.`,
});
}
}); });
const stepTwoRegisterSchema = z.object({
phone_number: z
.string({
required_error: 'Nomor telepon tidak boleh kosong',
invalid_type_error: 'Nomor telepon harus berupa string',
})
.min(1, 'Nomor telepon tidak boleh kosong')
.max(15, 'Nomor telepon tidak boleh lebih dari 15 karakter'),
referral_code: z
.string()
.min(1, 'Kode referral tidak boleh kosong')
.max(4, 'Kode referral tidak boleh lebih dari 4 karakter')
.optional(),
referred_by: z
.string()
.min(1, 'Kode referral tidak boleh kosong')
.max(50, 'Kode referral tidak boleh lebih dari 50 karakter')
.optional(),
student_type: z.string(),
});
export const authRegisterSchema = stepOneRegisterSchema.and(
stepTwoRegisterSchema
);
+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 './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 };

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