Compare commits
48
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b189ca3f81 | ||
|
|
02b40701f9 | ||
|
|
6e78b49731 | ||
|
|
3aa552afd6 | ||
|
|
b389235971 | ||
|
|
86290cc820 | ||
|
|
41712be56a | ||
|
|
240e684b53 | ||
|
|
b1a53df4a9 | ||
|
|
0709c3a2bd | ||
|
|
f2e1cead83 | ||
|
|
1b795f2dfc | ||
|
|
ce115f63e4 | ||
|
|
c907c696f5 | ||
|
|
de76020709 | ||
|
|
1e391a2932 | ||
|
|
a7c67cb77f | ||
|
|
dafdfba490 | ||
|
|
a741fbb8d4 | ||
|
|
0e3dc101d5 | ||
|
|
e805e5fd41 | ||
|
|
5bdf8f5a4d | ||
|
|
07f1fb0208 | ||
|
|
3958ae1098 | ||
|
|
885d156833 | ||
|
|
2746af0b3f | ||
|
|
745a2d2873 | ||
|
|
8887e3b0a9 | ||
|
|
638b64b715 | ||
|
|
d61cb4a215 | ||
|
|
272db82c8c | ||
|
|
9108ddb742 | ||
|
|
628e168342 | ||
|
|
9aff9e2dc8 | ||
|
|
585ad482f2 | ||
|
|
988476b409 | ||
|
|
38e6d4643b | ||
|
|
79cb9d81fc | ||
|
|
64e702df5d | ||
|
|
33c04707f8 | ||
|
|
f87dc4c782 | ||
|
|
b5428d8552 | ||
|
|
0a846b0512 | ||
|
|
3a950ea867 | ||
|
|
4693284b37 | ||
|
|
fe1928e8ef | ||
|
|
a9cc728ade | ||
|
|
2602657201 |
@@ -0,0 +1,4 @@
|
||||
VITE_API_URL=
|
||||
|
||||
CMS_SECRET=
|
||||
CMS_POSTGRES_URL=
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Test and Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['develop']
|
||||
pull_request:
|
||||
branches: ['develop']
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.14.0
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Nx Build
|
||||
run: npx nx run-many --target=build --all
|
||||
+11
@@ -51,3 +51,14 @@ storybook-static
|
||||
.env.prod
|
||||
.env.develop
|
||||
.env.staging
|
||||
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
|
||||
# Next.js
|
||||
.next
|
||||
out
|
||||
|
||||
Vendored
+2
-1
@@ -2,6 +2,7 @@
|
||||
"recommendations": [
|
||||
"nrwl.angular-console",
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint"
|
||||
"dbaeumer.vscode-eslint",
|
||||
"ms-playwright.playwright"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,101 +1,117 @@
|
||||
# ImphnenFrontendService
|
||||
# IMPHNEN Frontend Service
|
||||
|
||||
<a alt="Nx logo" href="https://nx.dev" target="_blank" rel="noreferrer"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-logo.png" width="45"></a>
|
||||
<p align="center">
|
||||
<img src="docs/logo.svg" alt="IMPHNEN">
|
||||
</p>
|
||||
|
||||
✨ Your new, shiny [Nx workspace](https://nx.dev) is ready ✨.
|
||||
This repository is a **monorepo** for all frontend services of IMPHNEN. The monorepo includes three main applications:
|
||||
|
||||
[Learn more about this workspace setup and its capabilities](https://nx.dev/getting-started/tutorials/react-monorepo-tutorial?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) or run `npx nx graph` to visually explore what was created. Now, let's get you up to speed!
|
||||
1. **Gacha** - Application for <a href="https://gacha.imphnen.dev/" target="_blank">gacha website</a>.
|
||||
2. **Backoffice** - Application for internal management.
|
||||
3. **Dimentorin** - Application for mentoring services.
|
||||
|
||||
## Run tasks
|
||||
## How to install
|
||||
|
||||
To run the dev server for your app, use:
|
||||
1. Clone this repository:
|
||||
```sh
|
||||
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git
|
||||
cd imphnen-frontend-service
|
||||
```
|
||||
2. Install all dependencies:
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
```sh
|
||||
npx nx serve dimentorin
|
||||
```
|
||||
## How to run
|
||||
|
||||
To create a production bundle:
|
||||
### Development
|
||||
|
||||
```sh
|
||||
npx nx build dimentorin
|
||||
```
|
||||
Use the following commands to run in development mode:
|
||||
|
||||
To see all available targets to run for a project, run:
|
||||
- **Gacha**:
|
||||
```sh
|
||||
npm run gacha:dev
|
||||
```
|
||||
- **Backoffice**:
|
||||
```sh
|
||||
npm run backoffice:dev
|
||||
```
|
||||
- **Dimentorin**:
|
||||
```sh
|
||||
npm run dimentorin:dev
|
||||
```
|
||||
|
||||
```sh
|
||||
npx nx show project dimentorin
|
||||
```
|
||||
### Build
|
||||
|
||||
These targets are either [inferred automatically](https://nx.dev/concepts/inferred-tasks?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) or defined in the `project.json` or `package.json` files.
|
||||
Use the following commands to build the applications:
|
||||
|
||||
[More about running tasks in the docs »](https://nx.dev/features/run-tasks?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||
- **Gacha**:
|
||||
```sh
|
||||
npm run gacha:build
|
||||
```
|
||||
- **Backoffice**:
|
||||
```sh
|
||||
npm run backoffice:build
|
||||
```
|
||||
- **Dimentorin**:
|
||||
```sh
|
||||
npm run dimentorin:build
|
||||
```
|
||||
|
||||
## Add new projects
|
||||
### Production
|
||||
|
||||
While you could add new projects to your workspace manually, you might want to leverage [Nx plugins](https://nx.dev/concepts/nx-plugins?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) and their [code generation](https://nx.dev/features/generate-code?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) feature.
|
||||
Use the following commands to run the applications in production mode:
|
||||
|
||||
Use the plugin's generator to create new projects.
|
||||
- **Gacha**:
|
||||
```sh
|
||||
npm run gacha:prod
|
||||
```
|
||||
- **Backoffice**:
|
||||
```sh
|
||||
npm run backoffice:prod
|
||||
```
|
||||
- **Dimentorin**:
|
||||
```sh
|
||||
npm run dimentorin:prod
|
||||
```
|
||||
|
||||
To generate a new application, use:
|
||||
### Storybook
|
||||
|
||||
```sh
|
||||
npx nx g @nx/react:app demo
|
||||
```
|
||||
This repository uses Storybook to develop, test, and document UI components in an isolated and interactive environment. Below are the commands to work with Storybook:
|
||||
|
||||
To generate a new library, use:
|
||||
- **Run Storybook**
|
||||
|
||||
This command starts Storybook in development mode, allowing you to view and test UI components interactively.
|
||||
|
||||
```sh
|
||||
npx nx g @nx/react:lib mylib
|
||||
```
|
||||
```sh
|
||||
npm run ui:storybook
|
||||
```
|
||||
|
||||
|
||||
You can use `npx nx list` to get a list of installed plugins. Then, run `npx nx list <plugin-name>` to learn about more specific capabilities of a particular plugin. Alternatively, [install Nx Console](https://nx.dev/getting-started/editor-setup?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) to browse plugins and generators in your IDE.
|
||||
- **Run Unit Test**
|
||||
|
||||
[Learn more about Nx plugins »](https://nx.dev/concepts/nx-plugins?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) | [Browse the plugin registry »](https://nx.dev/plugin-registry?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||
This command runs unit tests for the UI components to ensure they function as expected.
|
||||
|
||||
## Set up CI!
|
||||
```sh
|
||||
npm run ui:test
|
||||
```
|
||||
|
||||
### Step 1
|
||||
- **Build Components**
|
||||
|
||||
To connect to Nx Cloud, run the following command:
|
||||
This command generates a static build of Storybook, which can be deployed for sharing and documentation purposes.
|
||||
|
||||
```sh
|
||||
npx nx connect
|
||||
```
|
||||
```sh
|
||||
npm run ui:build
|
||||
```
|
||||
|
||||
Connecting to Nx Cloud ensures a [fast and scalable CI](https://nx.dev/ci/intro/why-nx-cloud?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) pipeline. It includes features such as:
|
||||
## How to contribute
|
||||
|
||||
- [Remote caching](https://nx.dev/ci/features/remote-cache?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||
- [Task distribution across multiple machines](https://nx.dev/ci/features/distribute-task-execution?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||
- [Automated e2e test splitting](https://nx.dev/ci/features/split-e2e-tasks?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||
- [Task flakiness detection and rerunning](https://nx.dev/ci/features/flaky-tasks?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||
1. Fork the repository and clone it locally.
|
||||
2. Create a new branch for a new feature or fix:
|
||||
```sh
|
||||
git checkout -b feat/feature-name
|
||||
```
|
||||
3. Make changes, commit, and push to your forked repository.
|
||||
4. Create a pull request to this repository `develop` branch.
|
||||
|
||||
### Step 2
|
||||
|
||||
Use the following command to configure a CI workflow for your workspace:
|
||||
|
||||
```sh
|
||||
npx nx g ci-workflow
|
||||
```
|
||||
|
||||
[Learn more about Nx on CI](https://nx.dev/ci/intro/ci-with-nx#ready-get-started-with-your-provider?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||
|
||||
## Install Nx Console
|
||||
|
||||
Nx Console is an editor extension that enriches your developer experience. It lets you run tasks, generate code, and improves code autocompletion in your IDE. It is available for VSCode and IntelliJ.
|
||||
|
||||
[Install Nx Console »](https://nx.dev/getting-started/editor-setup?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||
|
||||
## Useful links
|
||||
|
||||
Learn more:
|
||||
|
||||
- [Learn more about this workspace setup](https://nx.dev/getting-started/tutorials/react-monorepo-tutorial?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||
- [Learn about Nx on CI](https://nx.dev/ci/intro/ci-with-nx?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||
- [Releasing Packages with Nx release](https://nx.dev/features/manage-releases?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||
- [What are Nx plugins?](https://nx.dev/concepts/nx-plugins?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||
|
||||
And join the Nx community:
|
||||
- [Discord](https://go.nx.dev/community)
|
||||
- [Follow us on X](https://twitter.com/nxdevtools) or [LinkedIn](https://www.linkedin.com/company/nrwl)
|
||||
- [Our Youtube channel](https://www.youtube.com/@nxdevtools)
|
||||
- [Our blog](https://nx.dev/blog?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||
If you encounter any issues or problems, feel free to create a new Issue.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<base href="/" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<link rel="icon" type="image/x-icon" href="/logos/simple.svg" />
|
||||
<link rel="stylesheet" href="/src/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
authLoginSchema,
|
||||
TLoginRequest,
|
||||
usePostLogin,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const useLogin = () => {
|
||||
const postLogin = usePostLogin();
|
||||
const form = useForm<TLoginRequest>({
|
||||
resolver: zodResolver(authLoginSchema),
|
||||
mode: 'all',
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
postLogin.mutate(data, {
|
||||
onSuccess: () => toast.success("Login sukses"),
|
||||
onError: (error) => toast.error(error.message),
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
form,
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { useConfirmItem, useItem } from '../_hook/use-item';
|
||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||
|
||||
interface IModalAddItem {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleAddItem: () => void;
|
||||
handleAddItem?: () => Promise<boolean>;
|
||||
currentStep?: number;
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
@@ -46,91 +48,114 @@ interface IStepOneProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const StepOne = ({ nextStep }: IStepOneProps) => (
|
||||
<>
|
||||
<Modal.Header>
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Tambah Item Gacha
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Lengkapi detail di bawah ini untuk menambahkan item gacha
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-4">
|
||||
<InputField
|
||||
label="Nama Hadiah"
|
||||
type="text"
|
||||
placeholder="Masukkan Nama Hadiah"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<InputField
|
||||
label="Chance Rate"
|
||||
type="text"
|
||||
placeholder="Masukkan Chance Rate"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<InputField
|
||||
label="Foto Barang"
|
||||
type="file"
|
||||
placeholder=".jpg, .jpeg, atau .png"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
const { form, onSubmit } = useItem(nextStep);
|
||||
|
||||
<Button variant="primary" size="lg" className="w-full" onClick={nextStep}>
|
||||
Tambahkan Item
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Modal.Header>
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Tambah Item Gacha
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Lengkapi detail di bawah ini untuk menambahkan item gacha
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-4">
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Nama Hadiah"
|
||||
name="itemName"
|
||||
type="text"
|
||||
placeholder="Masukkan Nama Hadiah"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Quantity"
|
||||
name="quantity"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder="Masukkan Kuantitas Item"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Foto Barang"
|
||||
type="file"
|
||||
name="foto"
|
||||
placeholder=".jpg, .jpeg, atau .png"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={nextStep}
|
||||
>
|
||||
Tambahkan Item
|
||||
</Button>
|
||||
</form>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStepTwoProps {
|
||||
onClose: () => void;
|
||||
handleAddItem?: () => void;
|
||||
handleAddItem?: () => Promise<boolean>;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => (
|
||||
<>
|
||||
<Modal.Header className="mb-0 text-center items-center">
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Tambah Item
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Apakah kamu yakin ingin
|
||||
<br /> menambahkan item ini?
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex gap-4">
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
onClose();
|
||||
resetStep();
|
||||
}}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
handleAddItem && handleAddItem();
|
||||
onClose();
|
||||
resetStep();
|
||||
}}
|
||||
>
|
||||
Tambahkan
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => {
|
||||
const { onConfirm, onCancel } = useConfirmItem(
|
||||
onClose,
|
||||
resetStep,
|
||||
handleAddItem,
|
||||
{
|
||||
success: 'Item ditambahkan ke gacha item',
|
||||
error: 'Item gagal ditambahkan ke gacha item',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal.Header className="mb-0 text-center items-center">
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Tambah Item
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Apakah kamu yakin ingin
|
||||
<br /> menambahkan item ini?
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex gap-4">
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Tambahkan
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalAddItem;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { useConfirmItem, useItem } from '../_hook/use-item';
|
||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||
|
||||
interface IModalEditItem {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleEditItem?: () => void;
|
||||
handleEditItem?: () => Promise<boolean>;
|
||||
currentStep?: number;
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
@@ -46,91 +48,117 @@ interface IStepOneProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const StepOne = ({ nextStep }: IStepOneProps) => (
|
||||
<>
|
||||
<Modal.Header>
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Edit Item Gacha
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Silakan mengubah detail dari item yang diperlukan
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-4">
|
||||
<InputField
|
||||
label="Nama Hadiah"
|
||||
type="text"
|
||||
placeholder="Masukkan Nama Hadiah"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<InputField
|
||||
label="Chance Rate"
|
||||
type="text"
|
||||
placeholder="Masukkan Chance Rate"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<InputField
|
||||
label="Foto Barang"
|
||||
type="file"
|
||||
placeholder=".jpg, .jpeg, atau .png"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
const initialValues = {
|
||||
itemName: 'Hoodie IMPHNEN Official 2025',
|
||||
quantity: 10,
|
||||
};
|
||||
|
||||
<Button variant="primary" size="lg" className="w-full" onClick={nextStep}>
|
||||
Perbarui Item
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
const { form, onSubmit } = useItem(nextStep, initialValues);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal.Header>
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Edit Item Gacha
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Silakan mengubah detail dari item yang diperlukan
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex flex-col gap-8">
|
||||
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-4">
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Nama Hadiah"
|
||||
type="text"
|
||||
name="itemName"
|
||||
placeholder="Masukkan Nama Hadiah"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Quantity"
|
||||
type="number"
|
||||
name="quantity"
|
||||
placeholder="Masukkan Kuantitas Item"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Foto Barang"
|
||||
type="file"
|
||||
name="foto"
|
||||
placeholder=".jpg, .jpeg, atau .png"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={nextStep}
|
||||
>
|
||||
Perbarui Item
|
||||
</Button>
|
||||
</form>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStepTwoProps {
|
||||
onClose: () => void;
|
||||
handleEditItem?: () => void;
|
||||
handleEditItem?: () => Promise<boolean>;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const StepTwo = ({ onClose, handleEditItem, resetStep }: IStepTwoProps) => (
|
||||
<>
|
||||
<Modal.Header className="mb-0 text-center items-center">
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Update Item
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Apakah kamu yakin dengan
|
||||
<br /> perubahan yang dilakukan?
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex gap-4">
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
onClose();
|
||||
resetStep();
|
||||
}}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
handleEditItem && handleEditItem();
|
||||
onClose();
|
||||
resetStep();
|
||||
}}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
const StepTwo = ({ onClose, handleEditItem, resetStep }: IStepTwoProps) => {
|
||||
const { onConfirm, onCancel } = useConfirmItem(
|
||||
onClose,
|
||||
resetStep,
|
||||
handleEditItem,
|
||||
{
|
||||
success: 'Perubahan item berhasil dilakukan',
|
||||
error: 'Perubahan item gagal dilakukan',
|
||||
}
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Modal.Header className="mb-0 text-center items-center">
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Update Item
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Apakah kamu yakin dengan
|
||||
<br /> perubahan yang dilakukan?
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex gap-4">
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalEditItem;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import {
|
||||
gachaItemSchema,
|
||||
TGachaItem
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const useItem = (
|
||||
nextStep: () => void,
|
||||
initialValues?: TGachaItem
|
||||
) => {
|
||||
const form = useForm<TGachaItem>({
|
||||
resolver: zodResolver(gachaItemSchema),
|
||||
mode: 'all',
|
||||
defaultValues: initialValues,
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
console.log('Form data:', data);
|
||||
nextStep();
|
||||
});
|
||||
|
||||
return {
|
||||
form,
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
export const useConfirmItem = (
|
||||
onClose: () => void,
|
||||
resetStep: () => void,
|
||||
actionFunction?: () => Promise<boolean>,
|
||||
messages?: {
|
||||
success?: string;
|
||||
error?: string;
|
||||
}
|
||||
) => {
|
||||
const onConfirm = async () => {
|
||||
try {
|
||||
// const result = await actionFunction?.();
|
||||
// result ? toast.success(messages?.success) : toast.error(messages?.error);
|
||||
toast.success(messages?.success);
|
||||
onClose();
|
||||
resetStep();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error(messages?.error);
|
||||
}
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
onClose();
|
||||
resetStep();
|
||||
};
|
||||
|
||||
return {
|
||||
onConfirm,
|
||||
onCancel,
|
||||
};
|
||||
};
|
||||
@@ -31,20 +31,17 @@ export const Components: FC = (): ReactElement => {
|
||||
return (
|
||||
<Fragment>
|
||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
||||
{/* Dashboard Header */}
|
||||
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
||||
<h1 className="text-p2 font-semibold">Dashboard</h1>
|
||||
</header>
|
||||
|
||||
<div className="flex justify-between gap-[40px] p-8 bg-white rounded-md">
|
||||
<div className="w-full flex flex-col gap-[40px]">
|
||||
{/* Summary Section */}
|
||||
<section>
|
||||
<h2 className="text-p2 font-medium text-primary-500 mb-8">
|
||||
Summary
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Participants */}
|
||||
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
||||
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
||||
<UsergroupAddOutlined className="text-[20px]" />
|
||||
@@ -55,7 +52,6 @@ export const Components: FC = (): ReactElement => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Roll and Reroll */}
|
||||
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
||||
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
||||
<ReloadOutlined className="text-[20px]" />
|
||||
@@ -68,7 +64,6 @@ export const Components: FC = (): ReactElement => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Redeem */}
|
||||
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
||||
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
||||
<UserSwitchOutlined className="text-[20px]" />
|
||||
@@ -79,7 +74,6 @@ export const Components: FC = (): ReactElement => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Inactive Users */}
|
||||
<div className="bg-white rounded-lg shadow-sm py-4 px-6 flex items-center border border-neutral-100">
|
||||
<div className="mr-4 text-primary-500 bg-primary-100 p-[8px] rounded-md">
|
||||
<UsergroupDeleteOutlined className="text-[20px]" />
|
||||
@@ -94,7 +88,6 @@ export const Components: FC = (): ReactElement => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Gacha Items Section */}
|
||||
<section className="flex flex-col gap-8">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-p2 font-medium text-primary-500">
|
||||
@@ -111,21 +104,20 @@ export const Components: FC = (): ReactElement => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Gacha Items List */}
|
||||
<div className="flex flex-col gap-4 max-h-140 overflow-auto">
|
||||
{[1, 2, 3, 4, 5, 6].map((item) => (
|
||||
<div
|
||||
key={item}
|
||||
className="bg-white max-h-[80px] overflow-clip rounded-lg shadow-sm flex justify-between border border-neutral-100"
|
||||
className="bg-white overflow-clip rounded-lg shadow-sm flex justify-between border border-neutral-100"
|
||||
>
|
||||
<div className="flex flex-col py-4 px-6 gap-4">
|
||||
<div className="flex flex-col py-4 px-6 gap-[8px]">
|
||||
<div>
|
||||
<h3 className="text-p3 text-primary-500 font-medium">
|
||||
Lanyard IMPHNEN
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 text-label2 text-gray-500 mt-1">
|
||||
<div className="flex items-center justify-start gap-10 text-label2 text-gray-500 mt-1">
|
||||
<span>Prize {item}</span>
|
||||
<span>Chance Rate: (0.1%)</span>
|
||||
<span>Quantity: 10</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-start gap-2">
|
||||
@@ -148,12 +140,7 @@ export const Components: FC = (): ReactElement => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lebih baik gunakan gambar yang sudah di-clip dengan size height: 78px daripada hard-code object-position dan margin */}
|
||||
<img
|
||||
src="gacha-clip.webp"
|
||||
alt="Lanyard IMPHNEN"
|
||||
className="h-full"
|
||||
/>
|
||||
<img src="gacha-clip.webp" alt="Lanyard IMPHNEN" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -174,9 +161,6 @@ export const Components: FC = (): ReactElement => {
|
||||
currentStep={currentStep}
|
||||
isOpen={showModalAddItem}
|
||||
onClose={() => setShowModalAddItem(false)}
|
||||
handleAddItem={() => {
|
||||
console.log('Item added');
|
||||
}}
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
@@ -187,9 +171,6 @@ export const Components: FC = (): ReactElement => {
|
||||
currentStep={currentStep}
|
||||
isOpen={showModalEditItem}
|
||||
onClose={() => setShowModalEditItem(false)}
|
||||
handleEditItem={() => {
|
||||
console.log('Item edited');
|
||||
}}
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
@@ -199,9 +180,6 @@ export const Components: FC = (): ReactElement => {
|
||||
<ModalDeleteItem
|
||||
isOpen={showModalDeleteItem}
|
||||
onClose={() => setShowModalDeleteItem(false)}
|
||||
handleDeleteItem={() => {
|
||||
console.log('Item deleted');
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { useItem, useConfirmItem } from '../_hook/use-item';
|
||||
|
||||
interface IModalAddItem {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleAddItem?: () => Promise<boolean>;
|
||||
currentStep?: number;
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const ModalAddItem = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
currentStep,
|
||||
nextStep,
|
||||
resetStep,
|
||||
handleAddItem,
|
||||
}: IModalAddItem) => {
|
||||
return (
|
||||
<Modal
|
||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
||||
isOpen={isOpen}
|
||||
onClose={() => {
|
||||
onClose();
|
||||
resetStep();
|
||||
}}
|
||||
disableEscapeKeyDown={true}
|
||||
>
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
||||
{currentStep === 2 && (
|
||||
<StepTwo
|
||||
onClose={onClose}
|
||||
handleAddItem={handleAddItem}
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStepOneProps {
|
||||
nextStep: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
const { form, onSubmit } = useItem(nextStep);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal.Header>
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Tambah Item Roll Gacha
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Lengkapi detal di bawah ini, untuk menambahkan item gacha
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-4">
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Pilih Item"
|
||||
name="itemName"
|
||||
type="text"
|
||||
placeholder="Pilih item yang dimasukkan ke roll"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Quantity"
|
||||
name="quantity"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder="Masukkan Kuantitas Item"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Chance Rate"
|
||||
name="chanceRate"
|
||||
type="number"
|
||||
value={0.1}
|
||||
min={0.1}
|
||||
step={0.1}
|
||||
max={1}
|
||||
placeholder="Masukkan Chance Rate (0,1 - 1)"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button variant="primary" size="lg" className="w-full" type="submit">
|
||||
Tambahkan Item
|
||||
</Button>
|
||||
</form>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStepTwoProps {
|
||||
onClose: () => void;
|
||||
handleAddItem?: () => Promise<boolean>;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => {
|
||||
const { onConfirm, onCancel } = useConfirmItem(
|
||||
onClose,
|
||||
resetStep,
|
||||
handleAddItem,
|
||||
{
|
||||
success: 'Item ditambahkan ke roll gacha',
|
||||
error: 'Item gagal ditambahkan ke roll gacha',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal.Header className="mb-0 text-center items-center">
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Tambah ke Roll Gacha
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Apakah kamu yakin ingin
|
||||
<br /> menambahkan item ini ke roll gacha?
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex gap-4">
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Tambahkan
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalAddItem;
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
|
||||
interface IModalDeleteItem {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleDeleteItem?: () => void;
|
||||
}
|
||||
|
||||
const ModalDeleteItem = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
handleDeleteItem,
|
||||
}: IModalDeleteItem) => {
|
||||
return (
|
||||
<Modal
|
||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
closeButtonClassName="hidden"
|
||||
>
|
||||
<Modal.Header className="gap-8">
|
||||
<img
|
||||
src="/chibi-delete.webp"
|
||||
alt="Delete item?"
|
||||
width={148}
|
||||
className="self-center"
|
||||
/>
|
||||
<div className="text-center">
|
||||
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
|
||||
Delete Item
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Apakah kamu yakin untuk menghapus item ini?
|
||||
</p>
|
||||
</div>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex gap-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onClose}
|
||||
>
|
||||
Batal Hapus
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
handleDeleteItem && handleDeleteItem();
|
||||
}}
|
||||
>
|
||||
Hapus Item
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalDeleteItem;
|
||||
@@ -0,0 +1,168 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { useConfirmItem, useItem } from '../_hook/use-item';
|
||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||
|
||||
interface IModalUpdateItem {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleUpdateItem?: () => Promise<boolean>;
|
||||
currentStep?: number;
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const ModalUpdateItem = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
currentStep,
|
||||
nextStep,
|
||||
resetStep,
|
||||
handleUpdateItem,
|
||||
}: IModalUpdateItem) => {
|
||||
return (
|
||||
<Modal
|
||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
||||
isOpen={isOpen}
|
||||
onClose={() => {
|
||||
onClose();
|
||||
resetStep();
|
||||
}}
|
||||
disableEscapeKeyDown={true}
|
||||
>
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
||||
{currentStep === 2 && (
|
||||
<StepTwo
|
||||
onClose={onClose}
|
||||
handleUpdateItem={handleUpdateItem}
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStepOneProps {
|
||||
nextStep: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
const initialValues = {
|
||||
itemName: 'Hoodie IMPHNEN Official 2025',
|
||||
quantity: 10,
|
||||
chanceRate: 0.1,
|
||||
};
|
||||
|
||||
const { form, onSubmit } = useItem(nextStep, initialValues);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal.Header>
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Update Item Roll Gacha
|
||||
</h2>
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-4">
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Pilih Item"
|
||||
name="itemName"
|
||||
type="text"
|
||||
placeholder="Pilih item yang dimasukkan ke roll"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Quantity"
|
||||
name="quantity"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder="Masukkan Kuantitas Item"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Chance Rate"
|
||||
name="chanceRate"
|
||||
type="number"
|
||||
value={0.1}
|
||||
min={0.1}
|
||||
step={0.1}
|
||||
max={1}
|
||||
placeholder="Masukkan Chance Rate (0,1 - 1)"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={nextStep}
|
||||
>
|
||||
Perbarui Item
|
||||
</Button>
|
||||
</form>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStepTwoProps {
|
||||
onClose: () => void;
|
||||
handleUpdateItem?: () => Promise<boolean>;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const StepTwo = ({ onClose, handleUpdateItem, resetStep }: IStepTwoProps) => {
|
||||
const { onConfirm, onCancel } = useConfirmItem(
|
||||
onClose,
|
||||
resetStep,
|
||||
handleUpdateItem,
|
||||
{
|
||||
success: 'Perubahan item roll berhasil dilakukan',
|
||||
error: 'Perubahan item roll gagal dilakukan',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal.Header className="mb-0 text-center items-center">
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Update Item
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Apakah kamu yakin dengan
|
||||
<br /> perubahan yang dilakukan?
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex gap-4">
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalUpdateItem;
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import {
|
||||
gachaRollItemSchema,
|
||||
TGachaRollItem
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const useItem = (
|
||||
nextStep: () => void,
|
||||
initialValues?: TGachaRollItem
|
||||
) => {
|
||||
const form = useForm<TGachaRollItem>({
|
||||
resolver: zodResolver(gachaRollItemSchema),
|
||||
mode: 'all',
|
||||
defaultValues: initialValues,
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
console.log('Form data:', data);
|
||||
nextStep();
|
||||
});
|
||||
|
||||
return {
|
||||
form,
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
export const useConfirmItem = (
|
||||
onClose: () => void,
|
||||
resetStep: () => void,
|
||||
actionFunction?: () => Promise<boolean>,
|
||||
messages?: {
|
||||
success?: string;
|
||||
error?: string;
|
||||
}
|
||||
) => {
|
||||
const onConfirm = async () => {
|
||||
try {
|
||||
// const result = await actionFunction?.();
|
||||
// result ? toast.success(messages?.success) : toast.error(messages?.error);
|
||||
toast.success(messages?.success);
|
||||
onClose();
|
||||
resetStep();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error(messages?.error);
|
||||
}
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
onClose();
|
||||
resetStep();
|
||||
};
|
||||
|
||||
return {
|
||||
onConfirm,
|
||||
onCancel,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { FC, ReactElement } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
|
||||
|
||||
export const AppLayout: FC = (): ReactElement => {
|
||||
return (
|
||||
<div className="bg-primary-50 min-h-screen flex justify-center">
|
||||
<div className="bg-primary-50 min-h-screen w-full flex">
|
||||
<BackofficeSidebar />
|
||||
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLayout;
|
||||
@@ -0,0 +1,210 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
||||
import {
|
||||
SearchOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { DataTable } from '@imphnen-frontend-service/ui/organisms';
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
PaginationState,
|
||||
useReactTable,
|
||||
RowSelectionState,
|
||||
} from '@tanstack/react-table';
|
||||
import ModalAddItem from './_components/modal-add-item';
|
||||
import ModalUpdateItem from './_components/modal-update-item';
|
||||
import ModalDeleteItem from './_components/modal-delete-item';
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||
|
||||
interface GachaItem {
|
||||
id: number;
|
||||
name: string;
|
||||
chanceRate: number;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
const mockData: GachaItem[] = Array.from({ length: 90 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: 'Hoodie IMPHNEN Official 2025',
|
||||
chanceRate: 0.1,
|
||||
quantity: 10,
|
||||
}));
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
||||
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false);
|
||||
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
|
||||
|
||||
const {
|
||||
step: currentStep,
|
||||
nextStep,
|
||||
prevStep,
|
||||
resetStep,
|
||||
} = useQueryState('step', {
|
||||
defaultValue: 1,
|
||||
maxValue: 2,
|
||||
minValue: 1,
|
||||
});
|
||||
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 9,
|
||||
});
|
||||
|
||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
||||
|
||||
const columns: ColumnDef<GachaItem>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded"
|
||||
checked={table.getIsAllRowsSelected()}
|
||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded"
|
||||
checked={row.getIsSelected()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'No',
|
||||
accessorKey: 'id',
|
||||
},
|
||||
{
|
||||
header: 'Nama Item',
|
||||
accessorKey: 'name',
|
||||
},
|
||||
{
|
||||
header: 'Chance Rate',
|
||||
accessorKey: 'chanceRate',
|
||||
},
|
||||
{
|
||||
header: 'Quantity',
|
||||
accessorKey: 'quantity',
|
||||
},
|
||||
{
|
||||
header: 'Action',
|
||||
cell: () => (
|
||||
<div className="flex gap-[8px]">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowModalUpdateItem(true);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<EditOutlined /> Update
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowModalDeleteItem(true);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<DeleteOutlined /> Delete
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: mockData,
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
enableRowSelection: true,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
||||
manualPagination: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
||||
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
||||
<h1 className="text-p2 font-semibold">Gacha Roll</h1>
|
||||
</header>
|
||||
|
||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
||||
<div className="flex justify-between items-center gap-8 mb-2">
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
placeholder="Cari berdasarkan nama item"
|
||||
className="pl-12 w-full max-h-full"
|
||||
/>
|
||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
||||
<SearchOutlined />
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="md"
|
||||
className="flex gap-3 text-nowrap"
|
||||
onClick={() => {
|
||||
setShowModalAddItem(true);
|
||||
}}
|
||||
>
|
||||
<PlusOutlined />
|
||||
Tambah Item
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataTable data={mockData} columns={columns} table={table} />
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<ModalAddItem
|
||||
currentStep={currentStep}
|
||||
isOpen={showModalAddItem}
|
||||
onClose={() => setShowModalAddItem(false)}
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
<ModalUpdateItem
|
||||
currentStep={currentStep}
|
||||
isOpen={showModalUpdateItem}
|
||||
onClose={() => setShowModalUpdateItem(false)}
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
<ModalDeleteItem
|
||||
isOpen={showModalDeleteItem}
|
||||
onClose={() => setShowModalDeleteItem(false)}
|
||||
handleDeleteItem={() => {
|
||||
console.log('Item deleted');
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Components;
|
||||
@@ -1,10 +1,10 @@
|
||||
import { FC, ReactElement } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { InputField } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { useLogin } from './_hooks/use-login';
|
||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const navigate = useNavigate();
|
||||
const { form, onSubmit } = useLogin();
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-screen">
|
||||
@@ -13,21 +13,29 @@ export const Components: FC = (): ReactElement => {
|
||||
<h1 className="text-primary-500 text-p1 font-semibold">
|
||||
Welcome to IMPHNEN Backoffice
|
||||
</h1>
|
||||
<InputField
|
||||
label="Email"
|
||||
placeholder="Masukkan Email"
|
||||
type="email"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<InputField
|
||||
label="Password"
|
||||
placeholder="Masukkan password"
|
||||
type="password"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<Button onClick={() => navigate('/dashboard')}>Login</Button>
|
||||
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Email"
|
||||
placeholder="Masukkan Email"
|
||||
type="email"
|
||||
name="email"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Password"
|
||||
placeholder="Masukkan Password"
|
||||
type="password"
|
||||
name="password"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<Button type="submit" size="md" className="w-full">
|
||||
Login
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { useItem, useConfirmItem } from '../_hook/use-item';
|
||||
|
||||
interface IModalAddPermission {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleAddItem?: () => Promise<boolean>;
|
||||
currentStep?: number;
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const ModalAddPermission = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
currentStep,
|
||||
nextStep,
|
||||
resetStep,
|
||||
handleAddItem,
|
||||
}: IModalAddPermission) => {
|
||||
return (
|
||||
<Modal
|
||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-3 text-center"
|
||||
isOpen={isOpen}
|
||||
onClose={() => {
|
||||
onClose();
|
||||
resetStep();
|
||||
}}
|
||||
disableEscapeKeyDown={true}
|
||||
>
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
||||
{currentStep === 2 && (
|
||||
<StepTwo
|
||||
onClose={onClose}
|
||||
handleAddItem={handleAddItem}
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStepOneProps {
|
||||
nextStep: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
const { form, onSubmit } = useItem(nextStep);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal.Header>
|
||||
<h2 className="text-p1 font-semibold text-primary-500">
|
||||
Tambah Permissions
|
||||
</h2>
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Name"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Nama Permission"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
|
||||
<Button variant="primary" size="lg" className="w-full" type="submit">
|
||||
Tambah Permission
|
||||
</Button>
|
||||
</form>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStepTwoProps {
|
||||
onClose: () => void;
|
||||
handleAddItem?: () => Promise<boolean>;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => {
|
||||
const { onConfirm, onCancel } = useConfirmItem(
|
||||
onClose,
|
||||
resetStep,
|
||||
handleAddItem,
|
||||
{
|
||||
success: 'Data permissions berhasil ditambahkan',
|
||||
error: 'Data permissions gagal ditambahkan',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal.Header className="mb-7 text-center items-center">
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Tambah Permissions
|
||||
</h2>
|
||||
<p className="text-p3 text-center text-neutral-400">
|
||||
Apakah kamu yakin ingin
|
||||
<br /> menambahkan permission ini?
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex mb-0 gap-4">
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Tambahkan
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalAddPermission;
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { useConfirmItem } from '../_hook/use-item';
|
||||
|
||||
interface IModalDeletePermission {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleDelete?: () => Promise<boolean>;
|
||||
currentStep?: number;
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const ModalDeletePermission = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
resetStep,
|
||||
handleDelete,
|
||||
}: IModalDeletePermission) => {
|
||||
const { onConfirm } = useConfirmItem(onClose, resetStep, handleDelete, {
|
||||
success: 'Data permissions berhasil dihapus',
|
||||
error: 'Data permissions gagal dihapus',
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
closeButtonClassName="hidden"
|
||||
>
|
||||
<Modal.Header className="gap-8">
|
||||
<img
|
||||
src="/chibi-delete.webp"
|
||||
alt="Delete?"
|
||||
width={148}
|
||||
className="self-center"
|
||||
/>
|
||||
<div className="text-center">
|
||||
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
|
||||
Delete Permissions
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Apakah kamu yakin untuk menghapus permission ini? Menghapus data ini
|
||||
mungkin akan mempengaruhi fungsional sistem
|
||||
</p>
|
||||
</div>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex gap-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onClose}
|
||||
>
|
||||
Batal Hapus
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full bg-danger-500 hover:bg-danger-600"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Hapus Item
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalDeletePermission;
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { useConfirmItem } from '../_hook/use-item';
|
||||
|
||||
interface IModalUpdatePermission {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleUpdate?: () => Promise<boolean>;
|
||||
currentStep?: number;
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const ModalUpdatePermission = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
resetStep,
|
||||
handleUpdate,
|
||||
}: IModalUpdatePermission) => {
|
||||
const { onConfirm } = useConfirmItem(onClose, resetStep, handleUpdate, {
|
||||
success: 'Perubahan permissions berhasil dilakukan',
|
||||
error: 'Perubahan permissions gagal dilakukan',
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-3 text-center"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
disableEscapeKeyDown={true}
|
||||
>
|
||||
<Modal.Header>
|
||||
<h2 className="text-p1 font-semibold text-primary-500">
|
||||
Update Permissions
|
||||
</h2>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex flex-col gap-8">
|
||||
<InputField
|
||||
label="Name"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Nama Permission"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
type="submit"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Update Permission
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalUpdatePermission;
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
// import { zodResolver } from '@hookform/resolvers/zod';
|
||||
// import {
|
||||
// gachaRollItemSchema,
|
||||
// TGachaRollItem
|
||||
// } from '@imphnen-frontend-service/service';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const useItem = (
|
||||
nextStep: () => void,
|
||||
initialValues?: any
|
||||
) => {
|
||||
const form = useForm<any>({
|
||||
// resolver: zodResolver(),
|
||||
mode: 'all',
|
||||
defaultValues: initialValues,
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
console.log('Form data:', data);
|
||||
nextStep();
|
||||
});
|
||||
|
||||
return {
|
||||
form,
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
export const useConfirmItem = (
|
||||
onClose: () => void,
|
||||
resetStep: () => void,
|
||||
actionFunction?: () => Promise<boolean>,
|
||||
messages?: {
|
||||
success?: string;
|
||||
error?: string;
|
||||
}
|
||||
) => {
|
||||
const onConfirm = async () => {
|
||||
try {
|
||||
// const result = await actionFunction?.();
|
||||
// result ? toast.success(messages?.success) : toast.error(messages?.error);
|
||||
toast.success(messages?.success);
|
||||
onClose();
|
||||
resetStep();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error(messages?.error);
|
||||
}
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
onClose();
|
||||
resetStep();
|
||||
};
|
||||
|
||||
return {
|
||||
onConfirm,
|
||||
onCancel,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { FC, ReactElement } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
|
||||
|
||||
export const AppLayout: FC = (): ReactElement => {
|
||||
return (
|
||||
<div className="bg-primary-50 min-h-screen flex justify-center">
|
||||
<div className="bg-primary-50 min-h-screen w-full flex">
|
||||
<BackofficeSidebar />
|
||||
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLayout;
|
||||
@@ -0,0 +1,202 @@
|
||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
||||
import {
|
||||
SearchOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { DataTable } from '@imphnen-frontend-service/ui/organisms';
|
||||
import {
|
||||
ColumnDef,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import ModalAddPermission from './_components/modal-add-permission';
|
||||
import ModalUpdatePermission from './_components/modal-update-permission';
|
||||
import ModalDeletePermission from './_components/modal-delete-permission';
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||
import React from 'react';
|
||||
|
||||
interface Permission {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const mockData: Permission[] = [
|
||||
{ id: 1, name: 'Read' },
|
||||
{ id: 2, name: 'Create' },
|
||||
{ id: 3, name: 'Update' },
|
||||
{ id: 4, name: 'Delete' },
|
||||
];
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
||||
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false);
|
||||
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
|
||||
|
||||
const {
|
||||
step: currentStep,
|
||||
nextStep,
|
||||
prevStep,
|
||||
resetStep,
|
||||
} = useQueryState('step', {
|
||||
defaultValue: 1,
|
||||
maxValue: 2,
|
||||
minValue: 1,
|
||||
});
|
||||
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 9,
|
||||
});
|
||||
|
||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
||||
|
||||
const columns: ColumnDef<Permission>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded"
|
||||
checked={table.getIsAllRowsSelected()}
|
||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded"
|
||||
checked={row.getIsSelected()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'No',
|
||||
accessorKey: 'id',
|
||||
},
|
||||
{
|
||||
header: 'Name',
|
||||
accessorKey: 'name',
|
||||
},
|
||||
{
|
||||
header: 'Action',
|
||||
cell: () => (
|
||||
<div className="flex gap-[8px]">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowModalUpdateItem(true);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<EditOutlined /> Update
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowModalDeleteItem(true);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<DeleteOutlined /> Delete
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: mockData,
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
enableRowSelection: true,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
||||
manualPagination: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
||||
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
||||
<h1 className="text-p2 font-semibold">Permissions</h1>
|
||||
</header>
|
||||
|
||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
||||
<div className="flex justify-between items-center gap-8 mb-2">
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
placeholder="Cari berdasarkan nama permissions"
|
||||
className="pl-12 w-full max-h-full"
|
||||
/>
|
||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
||||
<SearchOutlined />
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="md"
|
||||
className="flex gap-3 text-nowrap"
|
||||
onClick={() => {
|
||||
setShowModalAddItem(true);
|
||||
}}
|
||||
>
|
||||
<PlusOutlined />
|
||||
Tambah Permissionss
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
data={mockData}
|
||||
columns={columns}
|
||||
pageSize={9}
|
||||
table={table}
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<ModalAddPermission
|
||||
currentStep={currentStep}
|
||||
isOpen={showModalAddItem}
|
||||
onClose={() => setShowModalAddItem(false)}
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
<ModalUpdatePermission
|
||||
isOpen={showModalUpdateItem}
|
||||
onClose={() => setShowModalUpdateItem(false)}
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
<ModalDeletePermission
|
||||
isOpen={showModalDeleteItem}
|
||||
onClose={() => setShowModalDeleteItem(false)}
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Components;
|
||||
@@ -0,0 +1,185 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { useItem, useConfirmItem } from '../_hook/use-item';
|
||||
|
||||
interface IModalAddRole {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleAdd?: () => Promise<boolean>;
|
||||
currentStep?: number;
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const ModalAddRole = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
currentStep,
|
||||
nextStep,
|
||||
resetStep,
|
||||
handleAdd,
|
||||
}: IModalAddRole) => {
|
||||
return (
|
||||
<Modal
|
||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-0 text-center"
|
||||
isOpen={isOpen}
|
||||
onClose={() => {
|
||||
onClose();
|
||||
resetStep();
|
||||
}}
|
||||
disableEscapeKeyDown={true}
|
||||
>
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
||||
{currentStep === 2 && (
|
||||
<StepTwo
|
||||
onClose={onClose}
|
||||
handleAdd={handleAdd}
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStepOneProps {
|
||||
nextStep: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const StepOne = ({ nextStep }: IStepOneProps) => {
|
||||
const { form, onSubmit } = useItem(nextStep);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal.Header>
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Tambah Roles
|
||||
</h2>
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<form onSubmit={onSubmit} className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-4">
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Nama Role"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Masukkan Nama Role"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 items-start overflow-auto">
|
||||
<span className="text-p3 font-medium text-neutral-800 sticky left-0">
|
||||
Permissions
|
||||
</span>
|
||||
<div className="flex gap-x-6 overflow-x-scroll">
|
||||
{[
|
||||
'Gacha Items',
|
||||
'Gacha Roll',
|
||||
'Roll',
|
||||
'Users',
|
||||
'Gacha Claim',
|
||||
].map((title) => (
|
||||
<div
|
||||
key={title}
|
||||
className="flex flex-col gap-4 select-none text-label2 font-medium text-neutral-900 "
|
||||
>
|
||||
<span className="text-nowrap text-label1">{title}</span>
|
||||
<div className="flex gap-[8px] items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`${title}-all`}
|
||||
className="rounded"
|
||||
/>
|
||||
<label htmlFor={`${title}-all`} className="text-nowrap">
|
||||
Check All
|
||||
</label>
|
||||
</div>
|
||||
<hr className="border-blue-200" />
|
||||
<div className="flex flex-col items-start gap-4 mb-4">
|
||||
<div className="flex gap-[8px] items-center">
|
||||
<input type="checkbox" id={`${title}-read`} />
|
||||
<label htmlFor={`${title}-read`}>Read</label>
|
||||
</div>
|
||||
<div className="flex gap-[8px] items-center">
|
||||
<input type="checkbox" id={`${title}-create`} />
|
||||
<label htmlFor={`${title}-create`}>Create</label>
|
||||
</div>
|
||||
<div className="flex gap-[8px] items-center">
|
||||
<input type="checkbox" id={`${title}-update`} />
|
||||
<label htmlFor={`${title}-update`}>Update</label>
|
||||
</div>
|
||||
<div className="flex gap-[8px] items-center">
|
||||
<input type="checkbox" id={`${title}-delete`} />
|
||||
<label htmlFor={`${title}-delete`}>Delete</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button variant="primary" size="lg" className="w-full" type="submit">
|
||||
Tambah Role
|
||||
</Button>
|
||||
</form>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStepTwoProps {
|
||||
onClose: () => void;
|
||||
handleAdd?: () => Promise<boolean>;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const StepTwo = ({ onClose, handleAdd, resetStep }: IStepTwoProps) => {
|
||||
const { onConfirm, onCancel } = useConfirmItem(
|
||||
onClose,
|
||||
resetStep,
|
||||
handleAdd,
|
||||
{
|
||||
success: 'Data role berhasil ditambahkan',
|
||||
error: 'Data role gagal ditambahkan',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal.Header className="mb-10 text-center items-center">
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Tambah Roles
|
||||
</h2>
|
||||
<p className="text-p3 text-center text-neutral-400">
|
||||
Apakah kamu yakin ingin
|
||||
<br /> menambahkan role ini?
|
||||
</p>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex mb-0 gap-4">
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Tambahkan
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalAddRole;
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { useConfirmItem } from '../_hook/use-item';
|
||||
|
||||
interface IModalDeletePermission {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleDelete?: () => Promise<boolean>;
|
||||
currentStep?: number;
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const ModalDeletePermission = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
resetStep,
|
||||
handleDelete,
|
||||
}: IModalDeletePermission) => {
|
||||
const { onConfirm } = useConfirmItem(onClose, resetStep, handleDelete, {
|
||||
success: 'Data roles berhasil dihapus',
|
||||
error: 'Data roles gagal dihapus',
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
closeButtonClassName="hidden"
|
||||
>
|
||||
<Modal.Header className="gap-8">
|
||||
<img
|
||||
src="/chibi-delete.webp"
|
||||
alt="Delete?"
|
||||
width={148}
|
||||
className="self-center"
|
||||
/>
|
||||
<div className="text-center">
|
||||
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
|
||||
Delete Roles
|
||||
</h2>
|
||||
<p className="text-p3 text-neutral-400">
|
||||
Apakah kamu yakin untuk menghapus role ini? Menghapus data ini
|
||||
mungkin akan mempengaruhi fungsional sistem
|
||||
</p>
|
||||
</div>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex gap-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={onClose}
|
||||
>
|
||||
Batal Hapus
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full bg-danger-500 hover:bg-danger-600"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Hapus Role
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalDeletePermission;
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { useConfirmItem } from '../_hook/use-item';
|
||||
|
||||
interface IModalUpdatePermission {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleUpdate?: () => Promise<boolean>;
|
||||
currentStep?: number;
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
resetStep: () => void;
|
||||
}
|
||||
|
||||
const ModalUpdatePermission = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
resetStep,
|
||||
handleUpdate,
|
||||
}: IModalUpdatePermission) => {
|
||||
const { onConfirm } = useConfirmItem(onClose, resetStep, handleUpdate, {
|
||||
success: 'Perubahan roles berhasil dilakukan',
|
||||
error: 'Perubahan roles gagal dilakukan',
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-0 text-center"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
disableEscapeKeyDown={true}
|
||||
>
|
||||
<Modal.Header>
|
||||
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
|
||||
Update Roles
|
||||
</h2>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-4">
|
||||
<InputField
|
||||
label="Nama Role"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Masukkan Nama Role"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 items-start overflow-auto">
|
||||
<span className="text-p3 font-medium text-neutral-800 sticky left-0">
|
||||
Permissions
|
||||
</span>
|
||||
<div className="flex gap-x-6 overflow-x-scroll">
|
||||
{['Gacha Items', 'Gacha Roll', 'Roll', 'Users', 'Gacha Claim'].map(
|
||||
(title) => (
|
||||
<div
|
||||
key={title}
|
||||
className="flex flex-col gap-4 select-none text-label2 font-medium text-neutral-900 "
|
||||
>
|
||||
<span className="text-nowrap text-label1">{title}</span>
|
||||
<div className="flex gap-[8px] items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`${title}-all`}
|
||||
className="rounded"
|
||||
/>
|
||||
<label htmlFor={`${title}-all`} className="text-nowrap">
|
||||
Check All
|
||||
</label>
|
||||
</div>
|
||||
<hr className="border-blue-200" />
|
||||
<div className="flex flex-col items-start gap-4 mb-4">
|
||||
<div className="flex gap-[8px] items-center">
|
||||
<input type="checkbox" id={`${title}-read`} />
|
||||
<label htmlFor={`${title}-read`}>Read</label>
|
||||
</div>
|
||||
<div className="flex gap-[8px] items-center">
|
||||
<input type="checkbox" id={`${title}-create`} />
|
||||
<label htmlFor={`${title}-create`}>Create</label>
|
||||
</div>
|
||||
<div className="flex gap-[8px] items-center">
|
||||
<input type="checkbox" id={`${title}-update`} />
|
||||
<label htmlFor={`${title}-update`}>Update</label>
|
||||
</div>
|
||||
<div className="flex gap-[8px] items-center">
|
||||
<input type="checkbox" id={`${title}-delete`} />
|
||||
<label htmlFor={`${title}-delete`}>Delete</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
type="submit"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Update Role
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalUpdatePermission;
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
// import { zodResolver } from '@hookform/resolvers/zod';
|
||||
// import {
|
||||
// gachaRollItemSchema,
|
||||
// TGachaRollItem
|
||||
// } from '@imphnen-frontend-service/service';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const useItem = (
|
||||
nextStep: () => void,
|
||||
initialValues?: any
|
||||
) => {
|
||||
const form = useForm<any>({
|
||||
// resolver: zodResolver(),
|
||||
mode: 'all',
|
||||
defaultValues: initialValues,
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
console.log('Form data:', data);
|
||||
nextStep();
|
||||
});
|
||||
|
||||
return {
|
||||
form,
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
export const useConfirmItem = (
|
||||
onClose: () => void,
|
||||
resetStep: () => void,
|
||||
actionFunction?: () => Promise<boolean>,
|
||||
messages?: {
|
||||
success?: string;
|
||||
error?: string;
|
||||
}
|
||||
) => {
|
||||
const onConfirm = async () => {
|
||||
try {
|
||||
// const result = await actionFunction?.();
|
||||
// result ? toast.success(messages?.success) : toast.error(messages?.error);
|
||||
toast.success(messages?.success);
|
||||
onClose();
|
||||
resetStep();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error(messages?.error);
|
||||
}
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
onClose();
|
||||
resetStep();
|
||||
};
|
||||
|
||||
return {
|
||||
onConfirm,
|
||||
onCancel,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { FC, ReactElement } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
|
||||
|
||||
export const AppLayout: FC = (): ReactElement => {
|
||||
return (
|
||||
<div className="bg-primary-50 min-h-screen flex justify-center">
|
||||
<div className="bg-primary-50 min-h-screen w-full flex">
|
||||
<BackofficeSidebar />
|
||||
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLayout;
|
||||
@@ -0,0 +1,203 @@
|
||||
import { FC, Fragment, ReactElement, useState } from 'react';
|
||||
import {
|
||||
SearchOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { DataTable } from '@imphnen-frontend-service/ui/organisms';
|
||||
import {
|
||||
ColumnDef,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import ModalAddRole from './_components/modal-add-role';
|
||||
import ModalUpdateRole from './_components/modal-update-role';
|
||||
import ModalDeleteRole from './_components/modal-delete-role';
|
||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
||||
import React from 'react';
|
||||
|
||||
interface Role {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const mockData: Role[] = [
|
||||
{ id: 1, name: 'Admin' },
|
||||
{ id: 2, name: 'Admin Pembayaran' },
|
||||
{ id: 3, name: 'Staff' },
|
||||
{ id: 4, name: 'Staff Aktivasi User' },
|
||||
{ id: 5, name: 'User' },
|
||||
];
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
||||
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false);
|
||||
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
|
||||
|
||||
const {
|
||||
step: currentStep,
|
||||
nextStep,
|
||||
prevStep,
|
||||
resetStep,
|
||||
} = useQueryState('step', {
|
||||
defaultValue: 1,
|
||||
maxValue: 2,
|
||||
minValue: 1,
|
||||
});
|
||||
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 9,
|
||||
});
|
||||
|
||||
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
||||
|
||||
const columns: ColumnDef<Role>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded"
|
||||
checked={table.getIsAllRowsSelected()}
|
||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded"
|
||||
checked={row.getIsSelected()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'ID',
|
||||
accessorKey: 'id',
|
||||
},
|
||||
{
|
||||
header: 'Roles Name',
|
||||
accessorKey: 'name',
|
||||
},
|
||||
{
|
||||
header: 'Action',
|
||||
cell: () => (
|
||||
<div className="flex gap-[8px]">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowModalUpdateItem(true);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<EditOutlined /> Update
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowModalDeleteItem(true);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<DeleteOutlined /> Delete
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: mockData,
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
enableRowSelection: true,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
||||
manualPagination: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
|
||||
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
|
||||
<h1 className="text-p2 font-semibold">Roles</h1>
|
||||
</header>
|
||||
|
||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
||||
<div className="flex justify-between items-center gap-8 mb-2">
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
placeholder="Cari berdasarkan nama roles"
|
||||
className="pl-12 w-full max-h-full"
|
||||
/>
|
||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
||||
<SearchOutlined />
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="md"
|
||||
className="flex gap-3 text-nowrap"
|
||||
onClick={() => {
|
||||
setShowModalAddItem(true);
|
||||
}}
|
||||
>
|
||||
<PlusOutlined />
|
||||
Tambah Role
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
data={mockData}
|
||||
columns={columns}
|
||||
pageSize={9}
|
||||
table={table}
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<ModalAddRole
|
||||
currentStep={currentStep}
|
||||
isOpen={showModalAddItem}
|
||||
onClose={() => setShowModalAddItem(false)}
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
<ModalUpdateRole
|
||||
isOpen={showModalUpdateItem}
|
||||
onClose={() => setShowModalUpdateItem(false)}
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
<ModalDeleteRole
|
||||
isOpen={showModalDeleteItem}
|
||||
onClose={() => setShowModalDeleteItem(false)}
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
resetStep={resetStep}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Components;
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
convertPagesToRoute,
|
||||
QueryProvider,
|
||||
} from '@imphnen-frontend-service/utils';
|
||||
import { Toaster } from 'sonner';
|
||||
import './index.css';
|
||||
|
||||
const files = import.meta.glob('./app/**/*(page|layout).tsx');
|
||||
@@ -34,6 +35,7 @@ if (!rootElement) throw new Error('Failed to find the root element');
|
||||
createRoot(rootElement).render(
|
||||
<StrictMode>
|
||||
<QueryProvider>
|
||||
<Toaster position="top-right" />
|
||||
<RouterProvider router={router} />
|
||||
</QueryProvider>
|
||||
</StrictMode>
|
||||
|
||||
@@ -8,11 +8,11 @@ export default defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../node_modules/.vite/apps/backoffice',
|
||||
server: {
|
||||
port: 4200,
|
||||
port: 3000,
|
||||
host: 'localhost',
|
||||
},
|
||||
preview: {
|
||||
port: 4300,
|
||||
port: 3001,
|
||||
host: 'localhost',
|
||||
},
|
||||
plugins: [react(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
authLoginSchema,
|
||||
TLoginRequest,
|
||||
usePostLogin,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const useLogin = () => {
|
||||
const postLogin = usePostLogin();
|
||||
const form = useForm<TLoginRequest>({
|
||||
resolver: zodResolver(authLoginSchema),
|
||||
mode: 'all',
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
postLogin.mutate(data, {
|
||||
onSuccess: () => toast.success("Login sukses"),
|
||||
onError: (error) => toast.error(error.message),
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
form,
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
@@ -1,46 +1,50 @@
|
||||
import { FC, ReactElement } from 'react';
|
||||
import { LoginBanner } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { ControlledInputField, LoginBanner } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { InputField } from '@imphnen-frontend-service/ui/molecules';
|
||||
import { useLogin } from '../../_hooks/use-login';
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons';
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const error = searchParams.get('error'); // Ambil nilai ?error=
|
||||
const { form, onSubmit } = useLogin();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
|
||||
<div className="bg-white min-w-[1120px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6">
|
||||
<div className="bg-white xl:min-w-[1120px] xl:min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6">
|
||||
<LoginBanner />
|
||||
<div className="border-2 border-primary-500/50 w-[596px] rounded-lg py-[70px] px-[96px] flex justify-center">
|
||||
<div className="w-[404px]">
|
||||
<div className="xl:border-2 xl:border-primary-500/50 md:w-[596px] rounded-lg md:py-[70px] md:px-[96px] flex justify-center">
|
||||
<div className="md:w-[404px]">
|
||||
<h2 className="text-4xl font-semibold text-primary-500 text-center mb-2">
|
||||
Hallo Minna-san
|
||||
</h2>
|
||||
<h5 className="text-xl font-medium text-primary-500 text-center">
|
||||
<h5 className="text-xl font-medium text-primary-500 text-center mb-5">
|
||||
Welcome to Dimentorin by IMPHNEN
|
||||
</h5>
|
||||
<InputField
|
||||
label="Email"
|
||||
error={error ? error : undefined}
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Masukkan email-mu, Senpai~! ✨ (Pastikan tidak typo, ya~ 😆)"
|
||||
/>
|
||||
<InputField
|
||||
label="password"
|
||||
error={error ? error : undefined}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
type="password"
|
||||
placeholder="Masukkan password rahasiamu!"
|
||||
/>
|
||||
<div className="flex justify-end my-5">
|
||||
<a href="/auth/forgot" className="text-primary-500 font-medium">
|
||||
Lupa Password ?
|
||||
</a>
|
||||
</div>
|
||||
<Button className="w-full">Enter Isekai</Button>
|
||||
<form onSubmit={onSubmit} className="flex flex-col gap-2">
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Email"
|
||||
size="lg"
|
||||
className="w-full mb-2"
|
||||
placeholder="Masukkan email-mu, Senpai~! ✨ (Pastikan tidak typo, ya~ 😆)"
|
||||
name={'email'}
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Password"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
type="password"
|
||||
placeholder="Masukkan password rahasiamu!"
|
||||
name={'password'}
|
||||
/>
|
||||
<div className="flex justify-end my-5">
|
||||
<a href="/auth/forgot" className="text-primary-500 font-medium">
|
||||
Lupa Password ?
|
||||
</a>
|
||||
</div>
|
||||
<Button className="w-full" type='submit'>Enter Isekai</Button>
|
||||
</form>
|
||||
|
||||
<div className="flex my-3 gap-3 justify-center">
|
||||
<h5>Belum Punya akun ?</h5>
|
||||
<a href="/auth/register" className="text-primary-500 font-medium">
|
||||
@@ -63,6 +67,13 @@ export const Components: FC = (): ReactElement => {
|
||||
width={24}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
className='xl:hidden w-full gap-3'
|
||||
variant='secondary'
|
||||
>
|
||||
<ArrowLeftOutlined />
|
||||
Kembali Ke Homepage
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,101 @@
|
||||
import { FC, ReactElement } from 'react';
|
||||
import { RegisterResetBanner } from "@imphnen-frontend-service/ui/organisms";
|
||||
import { RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { InputField } from '@imphnen-frontend-service/ui/molecules';
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
return (
|
||||
<div className='flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]'>
|
||||
<div className='bg-white min-w-[1120px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6'>
|
||||
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
|
||||
<div className="bg-white min-w-[1120px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6">
|
||||
<RegisterResetBanner />
|
||||
<div className='border-2 border-primary-500/50 w-[596px] rounded-lg py-[70px] px-[96px] flex justify-center'>
|
||||
<div className='w-[404px]'>
|
||||
</div>
|
||||
<div className="border-2 border-primary-500/50 w-[696px] rounded-lg py-[32px] px-[48px] flex justify-center">
|
||||
<div className="w-[704px]">
|
||||
<h3 className="mt-5 text-3xl font-semibold text-primary-500">
|
||||
Register
|
||||
</h3>
|
||||
<h5 className="text-primary-500 font-medium">
|
||||
Yosha~! Saatnya Bergabung dengan Dimentorin
|
||||
</h5>
|
||||
<div className="grid grid-flow-row-dense my-7 grid-cols-2 gap-2">
|
||||
<InputField
|
||||
label="First Name"
|
||||
size="lg"
|
||||
placeholder="Nama Depan"
|
||||
/>
|
||||
<InputField
|
||||
label="Last Name"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
placeholder="Nama Belakang"
|
||||
/>
|
||||
<InputField
|
||||
label="Email"
|
||||
size="lg"
|
||||
placeholder="Contoh : yourname@mail.com"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h4 className="font-medium">OTP Code</h4>
|
||||
<Input
|
||||
className="w-full"
|
||||
size="lg"
|
||||
placeholder="Kode OTP"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Button size="sm" className="mt-[30px]">
|
||||
Kirim OTP
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<InputField
|
||||
label="Password"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
type="password"
|
||||
placeholder="Buat password sekeren jurus ultimate-mu!"
|
||||
/>
|
||||
<h6 className="text-sm text-gray-500 mt-1">
|
||||
"Senpai~! Pastikan password-mu sekuat pertahanan kastil!"
|
||||
</h6>
|
||||
<h6 className="text-sm text-gray-500 mt-2">
|
||||
Tips membuat password yang OP:
|
||||
</h6>
|
||||
<h6 className="text-sm text-gray-500">
|
||||
- Minimal 8 karakter (semakin panjang, semakin power-up!{' '}
|
||||
<span role="img" aria-label="emoji">
|
||||
⚡
|
||||
</span>
|
||||
)
|
||||
</h6>
|
||||
<h6 className="text-sm text-gray-500">
|
||||
- Campur huruf besar, kecil, angka, dan simbol untuk kombinasi
|
||||
ultimate!
|
||||
<span role="img" aria-label="emoji">
|
||||
🔥
|
||||
</span>
|
||||
</h6>
|
||||
<h6 className="text-sm text-gray-500 mb-2">
|
||||
- Jangan pakai password yang gampang ditebak, nanti ketahuan
|
||||
musuh!
|
||||
<span role="img" aria-label="emoji">
|
||||
🚨
|
||||
</span>
|
||||
</h6>
|
||||
<InputField
|
||||
label="Repeat Password"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
type="password"
|
||||
placeholder="Ulangi password-mu, Senpai~!"
|
||||
/>
|
||||
<Button className="w-full mt-2">Linked Start !!!</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Components;
|
||||
export default Components;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { FC, ReactElement } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
export const AppLayout: FC = (): ReactElement => {
|
||||
return (
|
||||
<main className="bg-primary-50 min-h-screen">
|
||||
<Outlet />
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLayout;
|
||||
@@ -0,0 +1,55 @@
|
||||
import { FC, ReactElement } from 'react';
|
||||
import { RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { ArrowRightOutlined } from '@ant-design/icons';
|
||||
|
||||
export const Components: FC = (): ReactElement => {
|
||||
return (
|
||||
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
|
||||
<div className="bg-white min-w-[1220px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6">
|
||||
<RegisterResetBanner />
|
||||
<div className="border-2 border-primary-500/50 w-[696px] rounded-lg py-[32px] px-[48px] flex justify-center bg-primary-50">
|
||||
<div className="w-[704px] flex flex-col justify-center items-center">
|
||||
<h2 className="text-4xl text-primary-500 font-semibold">
|
||||
Register Successfull
|
||||
</h2>
|
||||
<h4 className="mt-3 text-primary-500 font-medium text-xl">
|
||||
Yosha~! Saatnya Bergabung dengan Dimentorin!
|
||||
</h4>
|
||||
<svg
|
||||
width="198"
|
||||
height="198"
|
||||
viewBox="0 0 198 198"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="mt-10"
|
||||
>
|
||||
<circle cx="99" cy="99" r="99" fill="#BCF8B0" />
|
||||
<rect
|
||||
x="31.1152"
|
||||
y="32.9141"
|
||||
width="133.971"
|
||||
height="133.971"
|
||||
rx="66.9857"
|
||||
fill="#35BA43"
|
||||
/>
|
||||
<path
|
||||
d="M127.935 75.8846H122.722C121.991 75.8846 121.297 76.2202 120.85 76.7945L90.0997 115.748L75.355 97.0657C75.1319 96.7825 74.8476 96.5535 74.5234 96.3959C74.1992 96.2384 73.8435 96.1563 73.483 96.1558H68.2697C67.77 96.1558 67.4941 96.7301 67.7999 97.1179L88.2277 122.998C89.1824 124.206 91.0171 124.206 91.9792 122.998L128.405 76.8392C128.711 76.4589 128.435 75.8846 127.935 75.8846Z"
|
||||
fill="#E0FBD8"
|
||||
/>
|
||||
</svg>
|
||||
<h4 className="mt-10 text-primary-500 font-medium text-xl">
|
||||
Kamu akan memasuki isekai dalam 10 dtk
|
||||
</h4>
|
||||
<Button className="gap-3 mt-10" size="lg">
|
||||
Masuk Isekai
|
||||
<ArrowRightOutlined />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Components;
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
convertPagesToRoute,
|
||||
QueryProvider,
|
||||
} from '@imphnen-frontend-service/utils';
|
||||
import { Toaster } from 'sonner';
|
||||
import './index.css';
|
||||
|
||||
const files = import.meta.glob('./app/**/*(page|layout).tsx');
|
||||
@@ -34,6 +35,7 @@ if (!rootElement) throw new Error('Failed to find the root element');
|
||||
createRoot(rootElement).render(
|
||||
<StrictMode>
|
||||
<QueryProvider>
|
||||
<Toaster position="top-right" />
|
||||
<RouterProvider router={router} />
|
||||
</QueryProvider>
|
||||
</StrictMode>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
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 { useQueryState } from '@imphnen-frontend-service/utils';
|
||||
import { useRegister } from '../../_hooks/use-register';
|
||||
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
import { TRegisterRequest } from '@imphnen-frontend-service/service';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
interface IModalFormRegisterProps {
|
||||
isOpen: boolean;
|
||||
@@ -8,6 +13,8 @@ interface IModalFormRegisterProps {
|
||||
}
|
||||
|
||||
const ModalFormRegister = ({ isOpen, onClose }: IModalFormRegisterProps) => {
|
||||
const { form, onSubmit, isStepOneValid } = useRegister();
|
||||
|
||||
const {
|
||||
step: currentStep,
|
||||
nextStep,
|
||||
@@ -19,6 +26,12 @@ const ModalFormRegister = ({ isOpen, onClose }: IModalFormRegisterProps) => {
|
||||
minValue: 1,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isStepOneValid && currentStep === 2) {
|
||||
prevStep();
|
||||
}
|
||||
}, [isStepOneValid, currentStep, prevStep]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7"
|
||||
@@ -37,74 +50,118 @@ const ModalFormRegister = ({ isOpen, onClose }: IModalFormRegisterProps) => {
|
||||
{currentStep === 1 ? 'Info Akun' : 'Informasi Pengiriman Hadiah'}
|
||||
</h2>
|
||||
</Modal.Header>
|
||||
<Modal.Content className="space-y-6">
|
||||
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
|
||||
{currentStep === 2 && (
|
||||
<StepTwo nextStep={nextStep} prevStep={prevStep} />
|
||||
)}
|
||||
<Modal.Content>
|
||||
<form onSubmit={onSubmit} className="space-y-6">
|
||||
{currentStep === 1 && (
|
||||
<StepOne
|
||||
form={form}
|
||||
nextStep={nextStep}
|
||||
isStepOneValid={isStepOneValid}
|
||||
/>
|
||||
)}
|
||||
{currentStep === 2 && <StepTwo form={form} prevStep={prevStep} />}
|
||||
</form>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStepOneProps {
|
||||
form: UseFormReturn<TRegisterRequest, any, TRegisterRequest>;
|
||||
nextStep: () => void;
|
||||
onClose: () => void;
|
||||
isStepOneValid: boolean;
|
||||
}
|
||||
|
||||
const StepOne = ({ nextStep, onClose }: IStepOneProps) => (
|
||||
<>
|
||||
<InputField
|
||||
label="Nama Lengkap"
|
||||
placeholder="Masukkan Nama Lengkap"
|
||||
type="text"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<InputField
|
||||
label="Email"
|
||||
placeholder="Masukkan Email Anda"
|
||||
type="email"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<InputField
|
||||
label="Password"
|
||||
placeholder="Masukkan Password"
|
||||
type="password"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<InputField
|
||||
label="Ulangi Password"
|
||||
placeholder="Masukkan Ulang Password"
|
||||
type="password"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<Button size="md" className="w-full" onClick={nextStep}>
|
||||
Selanjutnya
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
const StepOne = ({ form, nextStep, isStepOneValid }: IStepOneProps) => {
|
||||
return (
|
||||
<>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
name="fullname"
|
||||
label="Nama Lengkap"
|
||||
placeholder="Masukkan Nama Lengkap"
|
||||
type="text"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Email"
|
||||
placeholder="Masukkan Email Anda"
|
||||
type="email"
|
||||
name="email"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="Masukkan Password"
|
||||
type="password"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
name="confirm_password"
|
||||
label="Ulangi Password"
|
||||
placeholder="Masukkan Ulang Password"
|
||||
type="password"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<Button
|
||||
size="md"
|
||||
className="w-full"
|
||||
onClick={nextStep}
|
||||
disabled={!isStepOneValid}
|
||||
>
|
||||
Selanjutnya
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStepTwoProps {
|
||||
nextStep: () => void;
|
||||
form: UseFormReturn<TRegisterRequest, any, TRegisterRequest>;
|
||||
prevStep: () => void;
|
||||
}
|
||||
|
||||
const StepTwo = ({ nextStep, prevStep }: IStepTwoProps) => (
|
||||
const StepTwo = ({ form, prevStep }: IStepTwoProps) => (
|
||||
<>
|
||||
<InputField
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
name="phone_number"
|
||||
label="Nomor Telepon"
|
||||
placeholder="Masukkan Nomor Telepon Aktif"
|
||||
type="text"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<InputField
|
||||
label="Alamat Pengiriman"
|
||||
placeholder="Masukkan Alamat Pengiriman"
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
name="referral_code"
|
||||
label="Kode Referral"
|
||||
placeholder="Masukkan Kode Referral"
|
||||
type="text"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
name="referred_by"
|
||||
label="Referral By"
|
||||
placeholder="Masukkan Referral By"
|
||||
type="text"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
/>
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
name="student_type"
|
||||
label="Tipe Pelajar"
|
||||
placeholder="Masukkan Tipe Pelajar"
|
||||
type="text"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
@@ -113,7 +170,7 @@ const StepTwo = ({ nextStep, prevStep }: IStepTwoProps) => (
|
||||
<Button size="md" variant="text" className="w-[40%]" onClick={prevStep}>
|
||||
Kembali
|
||||
</Button>
|
||||
<Button size="md" className="w-full" onClick={nextStep}>
|
||||
<Button size="md" className="w-full" type="submit">
|
||||
Gacha Sekarang
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
authRegisterSchema,
|
||||
stepOneRegisterSchema,
|
||||
TRegisterRequest,
|
||||
usePostRegister,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { toast } from 'sonner';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const useRegister = () => {
|
||||
const postRegister = usePostRegister();
|
||||
const form = useForm<TRegisterRequest>({
|
||||
resolver: zodResolver(authRegisterSchema),
|
||||
mode: 'all',
|
||||
defaultValues: {
|
||||
fullname: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirm_password: '',
|
||||
phone_number: '',
|
||||
referral_code: '',
|
||||
referred_by: '',
|
||||
student_type: '',
|
||||
},
|
||||
});
|
||||
|
||||
const [stepValid, setStepValid] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = form.watch(() => {
|
||||
const { fullname, email, password, confirm_password } = form.getValues();
|
||||
const valid = stepOneRegisterSchema.safeParse({
|
||||
fullname,
|
||||
email,
|
||||
password,
|
||||
confirm_password,
|
||||
}).success;
|
||||
setStepValid(valid);
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [form]);
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
postRegister.mutate(data, {
|
||||
onSuccess: (data) => toast.success(data.message),
|
||||
onError: (error) => toast.error(error.message),
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
form,
|
||||
onSubmit,
|
||||
isStepOneValid: stepValid,
|
||||
};
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
convertPagesToRoute,
|
||||
QueryProvider,
|
||||
} from '@imphnen-frontend-service/utils';
|
||||
import { Toaster } from 'sonner';
|
||||
import './index.css';
|
||||
|
||||
const files = import.meta.glob('./app/**/*(page|layout).tsx');
|
||||
@@ -34,6 +35,7 @@ if (!rootElement) throw new Error('Failed to find the root element');
|
||||
createRoot(rootElement).render(
|
||||
<StrictMode>
|
||||
<QueryProvider>
|
||||
<Toaster />
|
||||
<RouterProvider router={router} />
|
||||
</QueryProvider>
|
||||
</StrictMode>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"jsc": {
|
||||
"target": "es2017",
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"decorators": true,
|
||||
"dynamicImport": true
|
||||
},
|
||||
"transform": {
|
||||
"decoratorMetadata": true,
|
||||
"legacyDecorator": true
|
||||
},
|
||||
"keepClassNames": true,
|
||||
"externalHelpers": true,
|
||||
"loose": true
|
||||
},
|
||||
"module": {
|
||||
"type": "commonjs"
|
||||
},
|
||||
"sourceMaps": true,
|
||||
"exclude": [
|
||||
"jest.config.ts",
|
||||
".*\\.spec.tsx?$",
|
||||
".*\\.test.tsx?$",
|
||||
"./src/jest-setup.ts$",
|
||||
"./**/jest-setup.ts$",
|
||||
".*.js$",
|
||||
".*.d.ts$"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { FlatCompat } from '@eslint/eslintrc';
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import js from '@eslint/js';
|
||||
import { fixupConfigRules } from '@eslint/compat';
|
||||
import nx from '@nx/eslint-plugin';
|
||||
import baseConfig from '../../eslint.config.mjs';
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: dirname(fileURLToPath(import.meta.url)),
|
||||
recommendedConfig: js.configs.recommended,
|
||||
});
|
||||
|
||||
export default [
|
||||
...fixupConfigRules(compat.extends('next')),
|
||||
...fixupConfigRules(compat.extends('next/core-web-vitals')),
|
||||
...baseConfig,
|
||||
...nx.configs['flat/react-typescript'],
|
||||
{
|
||||
ignores: ['.next/**/*'],
|
||||
},
|
||||
];
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
declare module '*.svg' {
|
||||
const content: any;
|
||||
export const ReactComponent: any;
|
||||
export default content;
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -0,0 +1,20 @@
|
||||
//@ts-check
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { composePlugins, withNx } = require('@nx/next');
|
||||
const { withPayload } = require('@payloadcms/next/withPayload');
|
||||
|
||||
/**
|
||||
* @type {import('@nx/next/plugins/with-nx').WithNxOptions}
|
||||
**/
|
||||
const nextConfig = {
|
||||
nx: {
|
||||
// Set this to true if you would like to to use SVGR
|
||||
// See: https://github.com/gregberge/svgr
|
||||
svgr: false,
|
||||
},
|
||||
};
|
||||
|
||||
const plugins = [withNx, withPayload];
|
||||
|
||||
module.exports = composePlugins(...plugins)(nextConfig);
|
||||
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "landing",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "apps/landing",
|
||||
"projectType": "application",
|
||||
"tags": [],
|
||||
"// targets": "to see all targets run: nx show project landing --web",
|
||||
"targets": {
|
||||
"serve": {
|
||||
"executor": "@nx/next:server",
|
||||
"options": {
|
||||
"buildTarget": "landing:build",
|
||||
"dev": true
|
||||
}
|
||||
},
|
||||
"build": {
|
||||
"executor": "@nx/next:build",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/apps/landing"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.0 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 159 KiB |
@@ -0,0 +1,67 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@imphnen-frontend-service/shadcn-ui/atoms';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import { useRef } from 'react';
|
||||
|
||||
export function CallToAction() {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, amount: 0.2 });
|
||||
|
||||
return (
|
||||
<section className="w-full py-20 md:py-32 relative overflow-hidden">
|
||||
{/* Background Elements */}
|
||||
<div className="absolute inset-0 -z-10">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-background to-primary/20" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(59,130,246,0.2),transparent_70%)]" />
|
||||
</div>
|
||||
|
||||
<div className="container px-4 md:px-6" ref={ref}>
|
||||
<motion.div
|
||||
className="max-w-4xl mx-auto rounded-2xl overflow-hidden border shadow-lg"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<div className="relative p-8 md:p-12 lg:p-16 bg-background">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-blue-400/5" />
|
||||
|
||||
<div className="relative z-10 text-center">
|
||||
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold mb-6">
|
||||
Siap Menjadi{' '}
|
||||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
||||
Programmer Handal?
|
||||
</span>
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground mb-8 max-w-2xl mx-auto">
|
||||
Bergabunglah dengan komunitas IMPHNEN sekarang dan mulai
|
||||
perjalanan programming mu dengan cara yang menyenangkan!
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90
|
||||
font-bold dark:text-black hover:text-black dark:hover:text-white cursor-pointer"
|
||||
>
|
||||
Gabung Discord
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="border-primary hover:bg-primary/10"
|
||||
>
|
||||
Join Facebook Group
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decorative Elements */}
|
||||
<div className="absolute -top-12 -left-12 w-24 h-24 rounded-full bg-primary/10 blur-2xl" />
|
||||
<div className="absolute -bottom-12 -right-12 w-24 h-24 rounded-full bg-blue-400/10 blur-2xl" />
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Button,
|
||||
FacebookIcon,
|
||||
InstagramIcon,
|
||||
MessageCircleIcon,
|
||||
} from '@imphnen-frontend-service/shadcn-ui/atoms';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import { useRef } from 'react';
|
||||
|
||||
export function Community() {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, amount: 0.2 });
|
||||
|
||||
const communities = [
|
||||
{
|
||||
icon: (
|
||||
<FacebookIcon className="h-6 w-6 text-blue-600 dark:text-blue-400" />
|
||||
),
|
||||
iconBg: 'bg-blue-100 dark:bg-blue-900',
|
||||
title: 'Facebook Group',
|
||||
description:
|
||||
'Bergabunglah dengan grup Facebook kami untuk diskusi santai dan berbagi artikel menarik.',
|
||||
buttonText: 'Gabung Sekarang',
|
||||
buttonLink: '#',
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<InstagramIcon className="h-6 w-6 text-blue-600 dark:text-blue-400" />
|
||||
),
|
||||
iconBg: 'bg-blue-100 dark:bg-blue-900',
|
||||
title: 'Instagram',
|
||||
description:
|
||||
'Ikuti kami di Instagram untuk tips programming, konten inspiratif, dan info event terbaru.',
|
||||
buttonText: 'Follow Kami',
|
||||
buttonLink: '#',
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<MessageCircleIcon className="h-6 w-6 text-blue-600 dark:text-blue-400" />
|
||||
),
|
||||
iconBg: 'bg-blue-100 dark:bg-blue-900',
|
||||
title: 'Discord Server',
|
||||
description:
|
||||
'Diskusikan langsung dengan sesama programmer dan dapatkan bantuan langsung dari para ahli.',
|
||||
buttonText: 'Join Server',
|
||||
buttonLink: '#',
|
||||
},
|
||||
];
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: { duration: 0.5 },
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
id="komunitas"
|
||||
className="w-full py-20 md:py-32 bg-muted relative overflow-hidden"
|
||||
>
|
||||
{/* Background Elements */}
|
||||
<div className="absolute inset-0 -z-10">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(59,130,246,0.1),transparent_70%)]" />
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,rgba(59,130,246,0.01)_1px,transparent_1px),linear-gradient(to_bottom,rgba(59,130,246,0.01)_1px,transparent_1px)] bg-[size:14px_14px]" />
|
||||
</div>
|
||||
|
||||
<div className="container px-4 md:px-6" ref={ref}>
|
||||
<div className="flex flex-col items-center justify-center space-y-4 text-center mb-16">
|
||||
<motion.div
|
||||
className="space-y-2"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<h2 className="text-3xl font-bold tracking-tighter md:text-4xl/tight lg:text-5xl">
|
||||
Komunitas{' '}
|
||||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
||||
Kami
|
||||
</span>
|
||||
</h2>
|
||||
<p className="max-w-[800px] mx-auto text-muted-foreground md:text-lg">
|
||||
Bergabunglah dengan ribuan programmer Indonesia yang saling
|
||||
membantu dan berbagi pengalaman.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="grid gap-8 md:grid-cols-3"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
>
|
||||
{communities.map((community, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
className="group relative overflow-hidden rounded-xl border bg-background p-6 transition-all hover:shadow-xl"
|
||||
variants={itemVariants}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-blue-400/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
|
||||
<div className="relative z-10">
|
||||
<div
|
||||
className={`mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full ${community.iconBg}`}
|
||||
>
|
||||
{community.icon}
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl font-bold">{community.title}</h3>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
{community.description}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full group-hover:bg-primary group-hover:text-primary-foreground transition-colors duration-300"
|
||||
>
|
||||
{community.buttonText}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="absolute -bottom-1 -right-1 w-20 h-20 bg-gradient-to-tl from-primary/20 to-transparent rounded-tl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
{/* Community Stats */}
|
||||
<motion.div
|
||||
className="mt-20 grid grid-cols-2 md:grid-cols-4 gap-8 text-center"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div className="text-4xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
||||
10K+
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Member Aktif</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-4xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
||||
50+
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Event Bulanan</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-4xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
||||
100+
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Mentor Profesional
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-4xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
||||
5K+
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Diskusi Mingguan
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
BookOpenIcon,
|
||||
CodeIcon,
|
||||
LaptopIcon,
|
||||
UsersIcon,
|
||||
} from '@imphnen-frontend-service/shadcn-ui/atoms';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import { useRef } from 'react';
|
||||
|
||||
export function Features() {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, amount: 0.2 });
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: <LaptopIcon className="h-6 w-6 text-primary" />,
|
||||
title: 'Belajar Tanpa Koding',
|
||||
description:
|
||||
'Pelajari konsep programming dengan cara yang mudah dipahami tanpa harus menulis kode yang rumit.',
|
||||
},
|
||||
{
|
||||
icon: <UsersIcon className="h-6 w-6 text-primary" />,
|
||||
title: 'Komunitas Supportif',
|
||||
description:
|
||||
'Bergabunglah dengan komunitas programmer Indonesia yang siap membantu dan berbagi pengalaman.',
|
||||
},
|
||||
{
|
||||
icon: <BookOpenIcon className="h-6 w-6 text-primary" />,
|
||||
title: 'Tutorial Interaktif',
|
||||
description:
|
||||
'Akses tutorial interaktif yang membuat konsep programming lebih mudah untuk dipahami.',
|
||||
},
|
||||
{
|
||||
icon: <CodeIcon className="h-6 w-6 text-primary" />,
|
||||
title: 'Proyek Praktis',
|
||||
description:
|
||||
'Terapkan pengetahuan Anda dalam proyek nyata dengan panduan langkah demi langkah.',
|
||||
},
|
||||
];
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: { duration: 0.5 },
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
id="fitur"
|
||||
className="w-full py-20 md:py-32 relative overflow-hidden"
|
||||
>
|
||||
{/* Background Elements */}
|
||||
<div className="absolute inset-0 -z-10">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-background via-background to-muted/50" />
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] rounded-full bg-gradient-to-tr from-primary/5 to-blue-400/5 blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className="container px-4 md:px-6" ref={ref}>
|
||||
<div className="flex flex-col items-center justify-center space-y-4 text-center mb-16">
|
||||
<motion.div
|
||||
className="space-y-2"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<div className="inline-block rounded-full bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary">
|
||||
Fitur Unggulan
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold tracking-tighter md:text-4xl/tight lg:text-5xl">
|
||||
Belajar programming dengan{' '}
|
||||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
||||
cara yang lebih baik
|
||||
</span>
|
||||
</h2>
|
||||
<p className="max-w-[800px] mx-auto text-muted-foreground md:text-lg">
|
||||
IMPHNEN hadir dengan berbagai fitur untuk membantu kamu menjadi
|
||||
programmer handal tanpa harus pusing dengan coding.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="grid gap-8 md:grid-cols-2 lg:grid-cols-4"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
>
|
||||
{features.map((feature, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
className="group relative overflow-hidden rounded-xl border bg-background/50 backdrop-blur-sm p-6 transition-all hover:shadow-md hover:shadow-primary/5 hover:border-primary/50"
|
||||
variants={itemVariants}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-blue-400/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 group-hover:bg-primary/20 transition-colors">
|
||||
{feature.icon}
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl font-bold">{feature.title}</h3>
|
||||
<p className="text-muted-foreground">{feature.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-0 left-0 h-1 w-0 bg-gradient-to-r from-primary to-blue-400 group-hover:w-full transition-all duration-300" />
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -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-white dark:text-black hover:text-black dark: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,145 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Button,
|
||||
MenuIcon,
|
||||
XIcon,
|
||||
} from '@imphnen-frontend-service/shadcn-ui/atoms';
|
||||
import { cn } from '@imphnen-frontend-service/utils';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { SimpleThemeToggle } from './simple-theme-toggle';
|
||||
|
||||
export function Header() {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > 10);
|
||||
};
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
'sticky top-0 z-50 w-full transition-all duration-300',
|
||||
isScrolled
|
||||
? 'bg-background/80 backdrop-blur-md border-b shadow-sm'
|
||||
: 'bg-transparent'
|
||||
)}
|
||||
>
|
||||
<div className="container flex h-16 items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative overflow-hidden rounded">
|
||||
<Image
|
||||
src="/logo.png"
|
||||
alt="IMPHNEN Logo"
|
||||
width={64}
|
||||
height={64}
|
||||
className="object-cover cursor-pointer"
|
||||
/>
|
||||
</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-white dark:text-black hover:text-black dark:hover:text-white cursor-pointer">
|
||||
Gabung Discord
|
||||
</Button>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="md:hidden"
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
>
|
||||
{mobileMenuOpen ? (
|
||||
<XIcon className="h-6 w-6" />
|
||||
) : (
|
||||
<MenuIcon className="h-6 w-6" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="md:hidden border-t bg-background/95 backdrop-blur-md">
|
||||
<nav className="container flex flex-col py-4 text-center">
|
||||
<Link
|
||||
href="#fitur"
|
||||
className="py-3 text-sm font-medium border-b border-border/50"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
Fitur
|
||||
</Link>
|
||||
<Link
|
||||
href="#komunitas"
|
||||
className="py-3 text-sm font-medium border-b border-border/50"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
Komunitas
|
||||
</Link>
|
||||
<Link
|
||||
href="#sumber-belajar"
|
||||
className="py-3 text-sm font-medium border-b border-border/50"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
Sumber Belajar
|
||||
</Link>
|
||||
<Link
|
||||
href="#testimoni"
|
||||
className="py-3 text-sm font-medium"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
Testimoni
|
||||
</Link>
|
||||
<Button className="mt-4 bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90 font-bold text-white dark:text-black hover:text-black dark:hover:text-white cursor-pointer">
|
||||
Gabung Discord
|
||||
</Button>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Button,
|
||||
CodeIcon,
|
||||
SparklesIcon,
|
||||
UsersIcon,
|
||||
} from '@imphnen-frontend-service/shadcn-ui/atoms';
|
||||
import { motion } from 'framer-motion';
|
||||
import Image from 'next/image';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function Hero() {
|
||||
const [scrollY, setScrollY] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setScrollY(window.scrollY);
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="relative w-full py-20 md:py-32 lg:py-40 overflow-hidden">
|
||||
{/* Background Elements */}
|
||||
<div className="absolute inset-0 -z-10 overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-gradient-to-b from-background to-background/50" />
|
||||
|
||||
{/* Animated Gradient Orbs */}
|
||||
<div
|
||||
className="absolute top-1/4 -left-20 w-80 h-80 rounded-full bg-gradient-to-r from-primary/20 to-blue-400/20 blur-3xl"
|
||||
style={{
|
||||
transform: `translate(${scrollY * 0.1}px, ${scrollY * -0.05}px)`,
|
||||
opacity: Math.max(0.2, 1 - scrollY * 0.001),
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute bottom-1/3 -right-20 w-80 h-80 rounded-full bg-gradient-to-r from-blue-400/20 to-primary/20 blur-3xl"
|
||||
style={{
|
||||
transform: `translate(${scrollY * -0.1}px, ${scrollY * 0.05}px)`,
|
||||
opacity: Math.max(0.2, 1 - scrollY * 0.001),
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Grid Pattern */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(rgba(59,130,246,0.05)_1px,transparent_1px),linear-gradient(to_right,rgba(59,130,246,0.05)_1px,transparent_1px)] bg-[size:40px_40px]" />
|
||||
</div>
|
||||
|
||||
<div className="container px-4 md:px-6 relative">
|
||||
<div className="grid gap-6 lg:grid-cols-2 lg:gap-12 items-center">
|
||||
<motion.div
|
||||
className="flex flex-col justify-center space-y-8"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<div className="inline-flex items-center rounded-full border px-3 py-1 text-sm">
|
||||
<SparklesIcon className="mr-1 h-3.5 w-3.5 text-primary" />
|
||||
<span>Komunitas Programmer Indonesia</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-4xl font-bold tracking-tighter sm:text-5xl md:text-6xl lg:text-7xl bg-clip-text text-transparent bg-gradient-to-r from-foreground via-foreground to-foreground/70">
|
||||
Programmer Handal, <br />
|
||||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
||||
Tanpa Ribet
|
||||
</span>
|
||||
</h1>
|
||||
<p className="max-w-[600px] text-muted-foreground md:text-xl">
|
||||
Temukan potensi programming Anda bersama komunitas yang
|
||||
mendukung, tutorial interaktif, dan sumber daya berkualitas
|
||||
tinggi.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<a href="https://facebook.com">
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90 transition-all duration-300
|
||||
font-bold text-white dark:text-black hover:text-black dark:hover:text-white cursor-pointer"
|
||||
>
|
||||
Mulai Belajar
|
||||
</Button>
|
||||
</a>
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="group relative overflow-hidden border-primary"
|
||||
>
|
||||
<span className="absolute inset-0 bg-gradient-to-r from-primary/10 to-blue-400/10 translate-y-full group-hover:translate-y-0 transition-transform duration-300"></span>
|
||||
<span className="relative">Gabung Discord</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="text-2xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
||||
100K+
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Member</div>
|
||||
</div>
|
||||
<div className="h-10 border-r border-border"></div>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="text-2xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
||||
500+
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Tutorial</div>
|
||||
</div>
|
||||
<div className="h-10 border-r border-border"></div>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="text-2xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
||||
24/7
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Yapping</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="relative mx-auto lg:ml-auto"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
>
|
||||
<div className="relative">
|
||||
{/* Decorative Elements */}
|
||||
<div className="absolute -top-6 -left-6 w-12 h-12 rounded-lg border border-primary/30 bg-background/50 backdrop-blur-sm flex items-center justify-center">
|
||||
<CodeIcon className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div className="absolute -bottom-6 -right-6 w-12 h-12 rounded-lg border border-blue-400/30 bg-background/50 backdrop-blur-sm flex items-center justify-center">
|
||||
<UsersIcon className="h-6 w-6 text-blue-400" />
|
||||
</div>
|
||||
|
||||
{/* Main Image */}
|
||||
<div className="relative z-10 rounded-2xl overflow-hidden border shadow-2xl">
|
||||
<div className="absolute inset-0 bg-gradient-to-tr from-primary/10 to-blue-400/10" />
|
||||
<Image
|
||||
src="/hero.jpeg"
|
||||
width={600}
|
||||
height={500}
|
||||
alt="IMPHNEN Programming Community"
|
||||
className="w-full h-auto object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
BookOpenIcon,
|
||||
Button,
|
||||
CodeIcon,
|
||||
Share2Icon,
|
||||
VideoIcon,
|
||||
} from '@imphnen-frontend-service/shadcn-ui/atoms';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
|
||||
import { useRef } from 'react';
|
||||
|
||||
export function LearningResources() {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, amount: 0.2 });
|
||||
|
||||
const resources = [
|
||||
{
|
||||
icon: <VideoIcon className="h-6 w-6 text-blue-600 dark:text-blue-400" />,
|
||||
iconBg: 'bg-blue-100 dark:bg-blue-900',
|
||||
title: 'Video Tutorial',
|
||||
description:
|
||||
'Belajar melalui tutorial video dari langkah awal hingga mahir.',
|
||||
buttonText: 'Lihat Semua Video',
|
||||
buttonLink: '#',
|
||||
color: 'from-blue-500 to-blue-700',
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<BookOpenIcon className="h-6 w-6 text-blue-600 dark:text-blue-400" />
|
||||
),
|
||||
iconBg: 'bg-blue-100 dark:bg-blue-900',
|
||||
title: 'Artikel & Tutorial',
|
||||
description:
|
||||
'Pelajari konsep programming melalui artikel yang disusun secara terstruktur.',
|
||||
buttonText: 'Baca Artikel',
|
||||
buttonLink: '#',
|
||||
color: 'from-blue-500 to-blue-700',
|
||||
},
|
||||
{
|
||||
icon: <CodeIcon className="h-6 w-6 text-blue-600 dark:text-blue-400" />,
|
||||
iconBg: 'bg-blue-100 dark:bg-blue-900',
|
||||
title: 'Tantangan Koding',
|
||||
description:
|
||||
'Uji kemampuan koding kamu dengan tantangan yang menyenangkan dan menantang.',
|
||||
buttonText: 'Mulai Tantangan',
|
||||
buttonLink: '#',
|
||||
color: 'from-blue-500 to-blue-700',
|
||||
},
|
||||
{
|
||||
icon: <Share2Icon className="h-6 w-6 text-blue-600 dark:text-blue-400" />,
|
||||
iconBg: 'bg-blue-100 dark:bg-blue-900',
|
||||
title: 'Sharing Session',
|
||||
description:
|
||||
'Ikuti sesi berbagi pengalaman dari programmer berpengalaman dan belajar dari pengalaman mereka.',
|
||||
buttonText: 'Jadwal Session',
|
||||
buttonLink: '#',
|
||||
color: 'from-blue-500 to-blue-700',
|
||||
},
|
||||
];
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: { duration: 0.5 },
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
id="sumber-belajar"
|
||||
className="w-full py-20 md:py-32 relative overflow-hidden"
|
||||
>
|
||||
{/* Background Elements */}
|
||||
<div className="absolute inset-0 -z-10">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-background via-background to-background" />
|
||||
<div className="absolute top-0 right-0 w-1/2 h-1/2 bg-gradient-to-bl from-primary/5 to-transparent blur-3xl" />
|
||||
<div className="absolute bottom-0 left-0 w-1/2 h-1/2 bg-gradient-to-tr from-blue-400/5 to-transparent blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className="container px-4 md:px-6" ref={ref}>
|
||||
<div className="flex flex-col items-center justify-center space-y-4 text-center mb-16">
|
||||
<motion.div
|
||||
className="space-y-2"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<h2 className="text-3xl font-bold tracking-tighter md:text-4xl/tight lg:text-5xl">
|
||||
Sumber{' '}
|
||||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
||||
Belajar
|
||||
</span>
|
||||
</h2>
|
||||
<p className="max-w-[800px] mx-auto text-muted-foreground md:text-lg">
|
||||
Akses berbagai materi belajar yang akan membantu kamu menguasai
|
||||
konsep programming dengan cara yang menyenangkan.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="grid gap-8 md:grid-cols-2 lg:grid-cols-4"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
>
|
||||
{resources.map((resource, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
className="group relative overflow-hidden rounded-xl border bg-background p-6 transition-all hover:shadow-lg"
|
||||
variants={itemVariants}
|
||||
>
|
||||
<div className="absolute top-0 left-0 h-1 w-full bg-gradient-to-r from-primary/50 to-blue-400/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
|
||||
<div className="relative z-10">
|
||||
<div
|
||||
className={`mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full ${resource.iconBg}`}
|
||||
>
|
||||
{resource.icon}
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl font-bold">{resource.title}</h3>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
{resource.description}
|
||||
</p>
|
||||
<Button
|
||||
variant="link"
|
||||
className="p-0 h-auto font-medium text-primary hover:text-primary/80"
|
||||
>
|
||||
{resource.buttonText} →
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="absolute -bottom-32 -right-32 w-64 h-64 bg-gradient-to-tl from-primary/10 to-transparent rounded-full opacity-0 group-hover:opacity-100 transition-all duration-500 group-hover:-translate-y-10 group-hover:-translate-x-10" />
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
{/* Featured Resource */}
|
||||
<motion.div
|
||||
className="mt-20 rounded-xl overflow-hidden border bg-background/50 backdrop-blur-sm"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
>
|
||||
<div className="grid md:grid-cols-2 gap-0">
|
||||
<div className="p-8 md:p-12 flex flex-col justify-center">
|
||||
<div className="inline-block rounded-full bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary mb-4">
|
||||
Rekomendasi Terbaik
|
||||
</div>
|
||||
<h3 className="text-2xl md:text-3xl font-bold mb-4">
|
||||
Kursus Lengkap Web Development
|
||||
</h3>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
Pelajari HTML, CSS, JavaScript, React, dan Node.js dalam satu
|
||||
kursus komprehensif yang dirancang untuk pemula hingga tingkat
|
||||
menengah.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Button className="bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90
|
||||
font-bold text-white dark:text-black hover:text-black dark:hover:text-white cursor-pointer">
|
||||
Mulai Kursus
|
||||
</Button>
|
||||
<Button variant="outline">Lihat Silabus</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative h-64 md:h-auto">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/20 to-blue-400/20" />
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-16 h-16 rounded-full bg-background/80 backdrop-blur-sm flex items-center justify-center cursor-pointer hover:bg-background transition-colors">
|
||||
<div className="w-0 h-0 border-t-8 border-t-transparent border-l-12 border-l-primary border-b-8 border-b-transparent ml-1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Button,
|
||||
MoonIcon,
|
||||
SunIcon,
|
||||
} from '@imphnen-frontend-service/shadcn-ui/atoms';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function SimpleThemeToggle() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(theme === 'dark' ? 'light' : 'dark');
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={toggleTheme}
|
||||
className="focus-visible:ring-0 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,190 @@
|
||||
'use client';
|
||||
|
||||
import { QuoteIcon } from '@imphnen-frontend-service/shadcn-ui/atoms';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import Image from 'next/image';
|
||||
import { useRef } from 'react';
|
||||
|
||||
export function Testimonials() {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, amount: 0.2 });
|
||||
|
||||
const testimonials = [
|
||||
{
|
||||
quote:
|
||||
'Saya yang tadinya tidak mengerti apa-apa tentang programming, sekarang bisa membuat website sendiri dengan mudah. Terima kasih IMPHNEN!',
|
||||
name: 'Budi Santoso',
|
||||
role: 'Web Developer Pemula',
|
||||
avatar: '/Budi Santoso.webp',
|
||||
},
|
||||
{
|
||||
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: '/Anita Ratna.webp',
|
||||
},
|
||||
{
|
||||
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: '/Dedi Permana.webp',
|
||||
},
|
||||
];
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: { duration: 0.5 },
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
id="testimoni"
|
||||
className="w-full py-20 md:py-32 bg-muted relative overflow-hidden"
|
||||
>
|
||||
{/* Background Elements */}
|
||||
<div className="absolute inset-0 -z-10">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,rgba(59,130,246,0.1),transparent_50%)]" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom,rgba(96,165,250,0.1),transparent_50%)]" />
|
||||
</div>
|
||||
|
||||
<div className="container px-4 md:px-6" ref={ref}>
|
||||
<div className="flex flex-col items-center justify-center space-y-4 text-center mb-16">
|
||||
<motion.div
|
||||
className="space-y-2"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<h2 className="text-3xl font-bold tracking-tighter md:text-4xl/tight lg:text-5xl">
|
||||
Testimoni{' '}
|
||||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
||||
Member
|
||||
</span>
|
||||
</h2>
|
||||
<p className="max-w-[800px] mx-auto text-muted-foreground md:text-lg">
|
||||
Apa kata mereka yang telah bergabung dengan komunitas IMPHNEN?
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="grid gap-8 md:grid-cols-3"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
>
|
||||
{testimonials.map((testimonial, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
className="group relative overflow-hidden rounded-xl border bg-background p-6 transition-all hover:shadow-lg"
|
||||
variants={itemVariants}
|
||||
>
|
||||
<div className="absolute top-6 right-6 text-primary/20 group-hover:text-primary/40 transition-colors">
|
||||
<QuoteIcon className="h-8 w-8" />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10">
|
||||
<p className="mb-6 text-muted-foreground italic">
|
||||
“{testimonial.quote}”
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative h-12 w-12 overflow-hidden rounded-full border-2 border-primary/20">
|
||||
<Image
|
||||
src={testimonial.avatar || '/placeholder.svg'}
|
||||
alt={testimonial.name}
|
||||
width={48}
|
||||
height={48}
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-bold">{testimonial.name}</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{testimonial.role}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute -bottom-1 -left-1 w-20 h-20 bg-gradient-to-tr from-primary/10 to-transparent rounded-tr-full opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
{/* Testimonial Stats */}
|
||||
<motion.div
|
||||
className="mt-20 rounded-xl overflow-hidden border bg-background/50 backdrop-blur-sm p-8 md:p-12"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
>
|
||||
<div className="grid md:grid-cols-2 gap-8 items-center">
|
||||
<div>
|
||||
<h3 className="text-2xl md:text-3xl font-bold mb-4">
|
||||
Bergabunglah dengan{' '}
|
||||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
||||
10,000+
|
||||
</span>{' '}
|
||||
programmer Indonesia lainnya
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Komunitas kami terus berkembang dengan programmer dari berbagai
|
||||
latar belakang dan tingkat keahlian. Bersama-sama, kita belajar,
|
||||
berbagi, dan tumbuh sebagai profesional.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="rounded-lg border p-4 text-center">
|
||||
<div className="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
||||
98%
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Tingkat Kemalasan
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border p-4 text-center">
|
||||
<div className="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
||||
4.9/5
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Rating Drama
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border p-4 text-center">
|
||||
<div className="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
||||
85%
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Mendapat Pekerjaan
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border p-4 text-center">
|
||||
<div className="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
||||
24/7
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Yapping</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import type { ThemeProviderProps } from 'next-themes';
|
||||
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { type Metadata } from 'next';
|
||||
import { Inter } from 'next/font/google';
|
||||
import '../../styles/globals.css';
|
||||
import { ThemeProvider } from './_components/theme-provider';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'IMPHNEN - Ingin Menjadi Programmer Handal?',
|
||||
description: 'Komunitas belajar programming untuk semua level',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="id" suppressHydrationWarning>
|
||||
<body className={inter.className}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { CallToAction } from './_components/call-to-action';
|
||||
import { Community } from './_components/community';
|
||||
import { Features } from './_components/features';
|
||||
import Footer from './_components/footer';
|
||||
import { Header } from './_components/header';
|
||||
import { Hero } from './_components/hero';
|
||||
import { LearningResources } from './_components/learning-resources';
|
||||
import { Testimonials } from './_components/testimonials';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<Header />
|
||||
<main className="flex-1">
|
||||
<Hero />
|
||||
<Features />
|
||||
<Community />
|
||||
<LearningResources />
|
||||
<Testimonials />
|
||||
<CallToAction />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 @@
|
||||
export const importMap = {};
|
||||
@@ -0,0 +1,19 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import '@payloadcms/next/css';
|
||||
import {
|
||||
REST_DELETE,
|
||||
REST_GET,
|
||||
REST_OPTIONS,
|
||||
REST_PATCH,
|
||||
REST_POST,
|
||||
REST_PUT,
|
||||
} from '@payloadcms/next/routes';
|
||||
import config from '../../../../payload.config';
|
||||
|
||||
export const GET = REST_GET(config);
|
||||
export const POST = REST_POST(config);
|
||||
export const DELETE = REST_DELETE(config);
|
||||
export const PATCH = REST_PATCH(config);
|
||||
export const PUT = REST_PUT(config);
|
||||
export const OPTIONS = REST_OPTIONS(config);
|
||||
@@ -0,0 +1,7 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import '@payloadcms/next/css';
|
||||
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes';
|
||||
import config from '../../../../payload.config';
|
||||
|
||||
export const GET = GRAPHQL_PLAYGROUND_GET(config);
|
||||
@@ -0,0 +1,8 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes';
|
||||
import config from '../../../../payload.config';
|
||||
|
||||
export const POST = GRAPHQL_POST(config);
|
||||
|
||||
export const OPTIONS = REST_OPTIONS(config);
|
||||
@@ -0,0 +1,35 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import '@payloadcms/next/css';
|
||||
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts';
|
||||
import type { ServerFunctionClient } from 'payload';
|
||||
import React from 'react';
|
||||
import config from '../../payload.config';
|
||||
|
||||
import { importMap } from './admin/importMap.js';
|
||||
import './custom.scss';
|
||||
|
||||
type Args = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const serverFunction: ServerFunctionClient = async function (args) {
|
||||
'use server';
|
||||
return handleServerFunctions({
|
||||
...args,
|
||||
config,
|
||||
importMap,
|
||||
});
|
||||
};
|
||||
|
||||
const Layout = ({ children }: Args) => (
|
||||
<RootLayout
|
||||
config={config}
|
||||
importMap={importMap}
|
||||
serverFunction={serverFunction}
|
||||
>
|
||||
{children}
|
||||
</RootLayout>
|
||||
);
|
||||
|
||||
export default Layout;
|
||||
@@ -0,0 +1,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,13 @@
|
||||
import type { CollectionConfig } from 'payload';
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
admin: {
|
||||
useAsTitle: 'email',
|
||||
},
|
||||
auth: true,
|
||||
fields: [
|
||||
// Email added by default
|
||||
// Add more fields as needed
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
import { getPayload } from 'payload';
|
||||
import config from '../payload.config';
|
||||
|
||||
export const payload = await getPayload({ config });
|
||||
@@ -0,0 +1,285 @@
|
||||
/* 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: {};
|
||||
globalsSelect: {};
|
||||
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` "auth".
|
||||
*/
|
||||
export interface Auth {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres';
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical';
|
||||
import path from 'path';
|
||||
import { buildConfig } from 'payload';
|
||||
import sharp from 'sharp';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { Media } from './collections/Media';
|
||||
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],
|
||||
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: [],
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
@import 'tailwindcss';
|
||||
@import "../../../../libs/shadcn-ui/src/index.css";
|
||||
@source "../../../../libs/shadcn-ui/src/atoms/**/*.{ts,tsx}";
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"emitDeclarationOnly": false,
|
||||
"esModuleInterop": true,
|
||||
"target": "ES2022",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.js",
|
||||
"**/*.jsx",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"../../apps/landing/.next/types/**/*.ts",
|
||||
"../../dist/apps/landing/.next/types/**/*.ts",
|
||||
"next-env.d.ts",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"jest.config.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.ts"
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 351 KiB |
+1
-1
@@ -12,7 +12,7 @@ export default [
|
||||
],
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'],
|
||||
rules: {
|
||||
'@nx/enforce-module-boundaries': [
|
||||
'error',
|
||||
|
||||
@@ -15,3 +15,64 @@ export const authLoginSchema = z.object({
|
||||
})
|
||||
.min(1, 'Password tidak boleh kosong'),
|
||||
});
|
||||
|
||||
export const stepOneRegisterSchema = z
|
||||
.object({
|
||||
email: z
|
||||
.string({
|
||||
required_error: 'Email tidak boleh kosong',
|
||||
invalid_type_error: 'Email harus berupa string',
|
||||
})
|
||||
.min(1, 'Email tidak boleh kosong')
|
||||
.email('Email harus valid'),
|
||||
fullname: z
|
||||
.string({
|
||||
required_error: 'Nama tidak boleh kosong',
|
||||
invalid_type_error: 'Nama harus berupa string',
|
||||
})
|
||||
.min(1, 'Nama tidak boleh kosong')
|
||||
.max(50, 'Nama tidak boleh lebih dari 50 karakter'),
|
||||
password: z
|
||||
.string({
|
||||
required_error: 'Password tidak boleh kosong',
|
||||
invalid_type_error: 'Password harus berupa string',
|
||||
})
|
||||
.min(1, 'Password tidak boleh kosong')
|
||||
.min(8, 'Password harus lebih dari 8 karakter')
|
||||
.max(50, 'Password tidak boleh lebih dari 50 karakter'),
|
||||
confirm_password: z
|
||||
.string({
|
||||
required_error: 'Konfirmasi password tidak boleh kosong',
|
||||
})
|
||||
.min(1, 'Konfirmasi password tidak boleh kosong')
|
||||
.max(50, 'Konfirmasi password tidak boleh lebih dari 50 karakter'),
|
||||
})
|
||||
.refine((data) => data.password === data.confirm_password, {
|
||||
message: 'Password dan Konfirmasi Password harus sama',
|
||||
path: ['confirm_password'],
|
||||
});
|
||||
|
||||
const stepTwoRegisterSchema = z.object({
|
||||
phone_number: z
|
||||
.string({
|
||||
required_error: 'Nomor telepon tidak boleh kosong',
|
||||
invalid_type_error: 'Nomor telepon harus berupa string',
|
||||
})
|
||||
.min(1, 'Nomor telepon tidak boleh kosong')
|
||||
.max(15, 'Nomor telepon tidak boleh lebih dari 15 karakter'),
|
||||
referral_code: z
|
||||
.string()
|
||||
.min(1, 'Kode referral tidak boleh kosong')
|
||||
.max(4, 'Kode referral tidak boleh lebih dari 4 karakter')
|
||||
.optional(),
|
||||
referred_by: z
|
||||
.string()
|
||||
.min(1, 'Kode referral tidak boleh kosong')
|
||||
.max(50, 'Kode referral tidak boleh lebih dari 50 karakter')
|
||||
.optional(),
|
||||
student_type: z.string(),
|
||||
});
|
||||
|
||||
export const authRegisterSchema = stepOneRegisterSchema.and(
|
||||
stepTwoRegisterSchema
|
||||
);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const gachaItemSchema = z.object({
|
||||
itemName: z
|
||||
.string({
|
||||
required_error: 'Nama item tidak boleh kosong',
|
||||
invalid_type_error: 'Nama item harus berupa string',
|
||||
})
|
||||
.min(1, 'Nama item tidak boleh kosong'),
|
||||
quantity: z
|
||||
.number({
|
||||
required_error: 'Quantity tidak boleh kosong',
|
||||
invalid_type_error: 'Quantity harus berupa angka',
|
||||
})
|
||||
.min(1, 'Quantity paling sedikit adalah 1'),
|
||||
foto: z
|
||||
.instanceof(File)
|
||||
.optional()
|
||||
.refine(
|
||||
(file) => !file || file.size <= 5000000, // 5MB in bytes
|
||||
'Ukuran file maksimal 5MB'
|
||||
)
|
||||
.refine(
|
||||
(file) => !file || ['image/jpeg', 'image/png', 'image/webp'].includes(file.type),
|
||||
'Format file harus JPG, PNG, atau WEBP'
|
||||
)
|
||||
});
|
||||
|
||||
export const gachaRollItemSchema = z.object({
|
||||
itemName: z
|
||||
.string({
|
||||
required_error: 'Nama item tidak boleh kosong',
|
||||
invalid_type_error: 'Nama item harus berupa string',
|
||||
})
|
||||
.min(1, 'Nama item tidak boleh kosong'),
|
||||
quantity: z
|
||||
.number({
|
||||
required_error: 'Quantity tidak boleh kosong',
|
||||
invalid_type_error: 'Quantity harus berupa angka',
|
||||
})
|
||||
.min(1, 'Quantity paling sedikit adalah 1'),
|
||||
chanceRate: z
|
||||
.number({
|
||||
required_error: 'Chance rate tidak boleh kosong',
|
||||
invalid_type_error: 'Chance rate harus berupa angka',
|
||||
})
|
||||
.min(0.1, 'Chance rate paling sedikit adalah 0,1')
|
||||
.max(1, 'Chance rate paling banyak adalah 1'),
|
||||
});
|
||||
@@ -1 +1,2 @@
|
||||
export * from './auth';
|
||||
export * from './gacha';
|
||||
|
||||
@@ -16,9 +16,14 @@ export type TLoginResponse = {
|
||||
};
|
||||
|
||||
export type TRegisterRequest = {
|
||||
fullname: string;
|
||||
email: string;
|
||||
fullname: string;
|
||||
password: string;
|
||||
phone_number: string;
|
||||
referral_code?: string;
|
||||
referred_by?: string;
|
||||
student_type: string;
|
||||
confirm_password: string;
|
||||
};
|
||||
|
||||
export type TVerifyEmailRequest = {
|
||||
|
||||
@@ -1 +1,11 @@
|
||||
export {};
|
||||
export type TGachaItem = {
|
||||
itemName: string;
|
||||
quantity: number;
|
||||
foto?: File;
|
||||
};
|
||||
|
||||
export type TGachaRollItem = {
|
||||
itemName: string;
|
||||
quantity: number;
|
||||
chanceRate: number;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@nx/react/babel",
|
||||
{
|
||||
"runtime": "automatic",
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": []
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# shadcn-ui
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test shadcn-ui` to execute the unit tests via [Vitest](https://vitest.dev/).
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@imphnen-frontend-service/service/components",
|
||||
"utils": "@imphnen-frontend-service/service/lib/utils",
|
||||
"ui": "@imphnen-frontend-service/service/components/ui",
|
||||
"lib": "@imphnen-frontend-service/service/lib",
|
||||
"hooks": "@imphnen-frontend-service/service/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
Generated
+1654
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@imphnen-frontend-service/shadcn-ui",
|
||||
"version": "0.0.1",
|
||||
"main": "./index.js",
|
||||
"types": "./index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./index.mjs",
|
||||
"require": "./index.js"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.2.0",
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.503.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss": "^4.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tw-animate-css": "^1.2.8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "shadcn-ui",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/shadcn-ui/src",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"// targets": "to see all targets run: nx show project shadcn-ui --web",
|
||||
"targets": {}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@imphnen-frontend-service/utils';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import * as React from 'react';
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||
outline:
|
||||
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
icon: 'h-9 w-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Button.displayName = 'Button';
|
||||
|
||||
export { Button, buttonVariants };
|
||||
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
LuArrowDown as ArrowDownIcon,
|
||||
LuBookOpen as BookOpenIcon,
|
||||
LuCode as CodeIcon,
|
||||
LuFacebook as FacebookIcon,
|
||||
LuInstagram as InstagramIcon,
|
||||
LuLaptop as LaptopIcon,
|
||||
LuMenu as MenuIcon,
|
||||
LuMessageCircle as MessageCircleIcon,
|
||||
LuMoon as MoonIcon,
|
||||
LuQuote as QuoteIcon,
|
||||
LuShare2 as Share2Icon,
|
||||
LuSparkles as SparklesIcon,
|
||||
LuSun as SunIcon,
|
||||
LuUsers as UsersIcon,
|
||||
LuVideo as VideoIcon,
|
||||
LuX as XIcon,
|
||||
} from 'react-icons/lu';
|
||||
|
||||
export {
|
||||
ArrowDownIcon,
|
||||
BookOpenIcon,
|
||||
CodeIcon,
|
||||
FacebookIcon,
|
||||
InstagramIcon,
|
||||
LaptopIcon,
|
||||
MenuIcon,
|
||||
MessageCircleIcon,
|
||||
MoonIcon,
|
||||
QuoteIcon,
|
||||
Share2Icon,
|
||||
SparklesIcon,
|
||||
SunIcon,
|
||||
UsersIcon,
|
||||
VideoIcon,
|
||||
XIcon,
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './button';
|
||||
export * from './icons';
|
||||
@@ -0,0 +1,134 @@
|
||||
@import 'tailwindcss';
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 1rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.623 0.214 259.815);
|
||||
--primary-foreground: oklch(0.97 0.014 254.604);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.623 0.214 259.815);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.623 0.214 259.815);
|
||||
--sidebar-primary-foreground: oklch(0.97 0.014 254.604);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.623 0.214 259.815);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.546 0.245 262.881);
|
||||
--primary-foreground: oklch(0.379 0.146 265.522);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.488 0.243 264.376);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.546 0.245 262.881);
|
||||
--sidebar-primary-foreground: oklch(0.379 0.146 265.522);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.488 0.243 264.376);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
/* https://github.com/tailwindlabs/tailwindcss/issues/13129 */
|
||||
.container {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
@media (min-width: 1536px) {
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './atoms/index';
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": false,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
}
|
||||
],
|
||||
"extends": "../../tsconfig.base.json"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user