diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..49d9f6e --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +VITE_API_URL=https://api.example.com diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/pull_request_template.md similarity index 100% rename from .github/PULL_REQUEST_TEMPLATE/pull_request_template.md rename to .github/pull_request_template.md diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml new file mode 100644 index 0000000..9c7190e --- /dev/null +++ b/.github/workflows/test-build.yml @@ -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 --legacy-peer-deps + + - name: Nx Build + run: npx nx run-many --target=build --all diff --git a/.gitignore b/.gitignore index 77912e8..412f51a 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,17 @@ Thumbs.db vite.config.*.timestamp* vitest.config.*.timestamp* -storybook-static \ No newline at end of file +storybook-static + + +.env +.env.prod +.env.develop +.env.staging + +# Nuxt dev/build outputs +.output +.data +.nuxt +.nitro +.cache \ No newline at end of file diff --git a/README.md b/README.md index 7d8118f..a924890 100644 --- a/README.md +++ b/README.md @@ -1,101 +1,117 @@ -# ImphnenFrontendService +# IMPHNEN Frontend Service - +

+ IMPHNEN +

-✨ 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 gacha website. +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 ` 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. diff --git a/apps/backoffice/index.html b/apps/backoffice/index.html index 3905403..52a9604 100644 --- a/apps/backoffice/index.html +++ b/apps/backoffice/index.html @@ -6,7 +6,7 @@ - + diff --git a/apps/backoffice/src/app/_hooks/use-login.ts b/apps/backoffice/src/app/_hooks/use-login.ts new file mode 100644 index 0000000..3c031d3 --- /dev/null +++ b/apps/backoffice/src/app/_hooks/use-login.ts @@ -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({ + 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, + }; +}; diff --git a/apps/backoffice/src/app/accounts/_components/modal-edit-account.tsx b/apps/backoffice/src/app/accounts/_components/modal-edit-account.tsx index adb382a..c7623bf 100644 --- a/apps/backoffice/src/app/accounts/_components/modal-edit-account.tsx +++ b/apps/backoffice/src/app/accounts/_components/modal-edit-account.tsx @@ -1,5 +1,5 @@ import { Button } from '@imphnen-frontend-service/ui/atoms'; -import { InputForm, Modal } from '@imphnen-frontend-service/ui/molecules'; +import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules'; import { useState } from 'react'; interface IModalEditAccount { @@ -63,7 +63,7 @@ const StepOne = ({ nextStep }: IStepOneProps) => {
- { size="lg" className="w-full" /> - { size="lg" className="w-full" /> - { size="lg" className="w-full" /> - void; - handleAddItem: () => void; + handleAddItem?: () => Promise; currentStep?: number; nextStep: () => void; prevStep: () => void; @@ -46,91 +48,114 @@ interface IStepOneProps { onClose: () => void; } -const StepOne = ({ nextStep }: IStepOneProps) => ( - <> - -

- Tambah Item Gacha -

-

- Lengkapi detail di bawah ini untuk menambahkan item gacha -

-
- -
- - - -
+const StepOne = ({ nextStep }: IStepOneProps) => { + const { form, onSubmit } = useItem(nextStep); - -
- -); + return ( + <> + +

+ Tambah Item Gacha +

+

+ Lengkapi detail di bawah ini untuk menambahkan item gacha +

+
+ +
+
+ + + +
+ + +
+
+ + ); +}; interface IStepTwoProps { onClose: () => void; - handleAddItem?: () => void; + handleAddItem?: () => Promise; resetStep: () => void; } -const StepTwo = ({ onClose, handleAddItem, resetStep }: IStepTwoProps) => ( - <> - -

- Tambah Item -

-

- Apakah kamu yakin ingin -
menambahkan item ini? -

-
- - - - - -); +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 ( + <> + +

+ Tambah Item +

+

+ Apakah kamu yakin ingin +
menambahkan item ini? +

+
+ + + + + + ); +}; export default ModalAddItem; diff --git a/apps/backoffice/src/app/dashboard/_components/modal-edit-item.tsx b/apps/backoffice/src/app/dashboard/_components/modal-edit-item.tsx index 8e88a89..19a22fe 100644 --- a/apps/backoffice/src/app/dashboard/_components/modal-edit-item.tsx +++ b/apps/backoffice/src/app/dashboard/_components/modal-edit-item.tsx @@ -1,10 +1,12 @@ import { Button } from '@imphnen-frontend-service/ui/atoms'; -import { InputForm, Modal } from '@imphnen-frontend-service/ui/molecules'; +import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules'; +import { useConfirmItem, useItem } from '../_hook/use-item'; +import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'; interface IModalEditItem { isOpen: boolean; onClose: () => void; - handleEditItem?: () => void; + handleEditItem?: () => Promise; currentStep?: number; nextStep: () => void; prevStep: () => void; @@ -46,91 +48,117 @@ interface IStepOneProps { onClose: () => void; } -const StepOne = ({ nextStep }: IStepOneProps) => ( - <> - -

- Edit Item Gacha -

-

- Silakan mengubah detail dari item yang diperlukan -

-
- -
- - - -
+const StepOne = ({ nextStep }: IStepOneProps) => { + const initialValues = { + itemName: 'Hoodie IMPHNEN Official 2025', + quantity: 10, + }; - -
- -); + const { form, onSubmit } = useItem(nextStep, initialValues); + + return ( + <> + +

+ Edit Item Gacha +

+

+ Silakan mengubah detail dari item yang diperlukan +

+
+ +
+
+ + + +
+ + +
+
+ + ); +}; interface IStepTwoProps { onClose: () => void; - handleEditItem?: () => void; + handleEditItem?: () => Promise; resetStep: () => void; } -const StepTwo = ({ onClose, handleEditItem, resetStep }: IStepTwoProps) => ( - <> - -

- Update Item -

-

- Apakah kamu yakin dengan -
perubahan yang dilakukan? -

-
- - - - - -); +const StepTwo = ({ onClose, handleEditItem, resetStep }: IStepTwoProps) => { + const { onConfirm, onCancel } = useConfirmItem( + onClose, + resetStep, + handleEditItem, + { + success: 'Perubahan item berhasil dilakukan', + error: 'Perubahan item gagal dilakukan', + } + ); + return ( + <> + +

+ Update Item +

+

+ Apakah kamu yakin dengan +
perubahan yang dilakukan? +

+
+ + + + + + ); +}; export default ModalEditItem; diff --git a/apps/backoffice/src/app/dashboard/_hook/use-item.ts b/apps/backoffice/src/app/dashboard/_hook/use-item.ts new file mode 100644 index 0000000..d432d56 --- /dev/null +++ b/apps/backoffice/src/app/dashboard/_hook/use-item.ts @@ -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({ + 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, + 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, + }; +}; diff --git a/apps/backoffice/src/app/dashboard/page.tsx b/apps/backoffice/src/app/dashboard/page.tsx index d337e53..be30690 100644 --- a/apps/backoffice/src/app/dashboard/page.tsx +++ b/apps/backoffice/src/app/dashboard/page.tsx @@ -31,20 +31,17 @@ export const Components: FC = (): ReactElement => { return (
- {/* Dashboard Header */}

Dashboard

- {/* Summary Section */}

Summary

- {/* Participants */}
@@ -55,7 +52,6 @@ export const Components: FC = (): ReactElement => {
- {/* Roll and Reroll */}
@@ -68,7 +64,6 @@ export const Components: FC = (): ReactElement => {
- {/* Redeem */}
@@ -79,7 +74,6 @@ export const Components: FC = (): ReactElement => {
- {/* Inactive Users */}
@@ -94,7 +88,6 @@ export const Components: FC = (): ReactElement => {
- {/* Gacha Items Section */}

@@ -111,21 +104,20 @@ export const Components: FC = (): ReactElement => {

- {/* Gacha Items List */}
{[1, 2, 3, 4, 5, 6].map((item) => (
-
+

Lanyard IMPHNEN

-
+
Prize {item} - Chance Rate: (0.1%) + Quantity: 10
@@ -148,12 +140,7 @@ export const Components: FC = (): ReactElement => {
- {/* Lebih baik gunakan gambar yang sudah di-clip dengan size height: 78px daripada hard-code object-position dan margin */} - Lanyard IMPHNEN + Lanyard IMPHNEN
))}
@@ -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 => { setShowModalDeleteItem(false)} - handleDeleteItem={() => { - console.log('Item deleted'); - }} /> ); diff --git a/apps/backoffice/src/app/gacha-roll/_components/modal-add-item.tsx b/apps/backoffice/src/app/gacha-roll/_components/modal-add-item.tsx new file mode 100644 index 0000000..46a59ea --- /dev/null +++ b/apps/backoffice/src/app/gacha-roll/_components/modal-add-item.tsx @@ -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; + currentStep?: number; + nextStep: () => void; + prevStep: () => void; + resetStep: () => void; +} + +const ModalAddItem = ({ + isOpen, + onClose, + currentStep, + nextStep, + resetStep, + handleAddItem, +}: IModalAddItem) => { + return ( + { + onClose(); + resetStep(); + }} + disableEscapeKeyDown={true} + > + {currentStep === 1 && } + {currentStep === 2 && ( + + )} + + ); +}; + +interface IStepOneProps { + nextStep: () => void; + onClose: () => void; +} + +const StepOne = ({ nextStep }: IStepOneProps) => { + const { form, onSubmit } = useItem(nextStep); + + return ( + <> + +

+ Tambah Item Roll Gacha +

+

+ Lengkapi detal di bawah ini, untuk menambahkan item gacha +

+
+ +
+
+ + + +
+ + +
+
+ + ); +}; + +interface IStepTwoProps { + onClose: () => void; + handleAddItem?: () => Promise; + 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 ( + <> + +

+ Tambah ke Roll Gacha +

+

+ Apakah kamu yakin ingin +
menambahkan item ini ke roll gacha? +

+
+ + + + + + ); +}; + +export default ModalAddItem; diff --git a/apps/backoffice/src/app/gacha-roll/_components/modal-delete-item.tsx b/apps/backoffice/src/app/gacha-roll/_components/modal-delete-item.tsx new file mode 100644 index 0000000..3663ee0 --- /dev/null +++ b/apps/backoffice/src/app/gacha-roll/_components/modal-delete-item.tsx @@ -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 ( + + + Delete item? +
+

+ Delete Item +

+

+ Apakah kamu yakin untuk menghapus item ini? +

+
+
+ + + + +
+ ); +}; + +export default ModalDeleteItem; diff --git a/apps/backoffice/src/app/gacha-roll/_components/modal-update-item.tsx b/apps/backoffice/src/app/gacha-roll/_components/modal-update-item.tsx new file mode 100644 index 0000000..770f867 --- /dev/null +++ b/apps/backoffice/src/app/gacha-roll/_components/modal-update-item.tsx @@ -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; + currentStep?: number; + nextStep: () => void; + prevStep: () => void; + resetStep: () => void; +} + +const ModalUpdateItem = ({ + isOpen, + onClose, + currentStep, + nextStep, + resetStep, + handleUpdateItem, +}: IModalUpdateItem) => { + return ( + { + onClose(); + resetStep(); + }} + disableEscapeKeyDown={true} + > + {currentStep === 1 && } + {currentStep === 2 && ( + + )} + + ); +}; + +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 ( + <> + +

+ Update Item Roll Gacha +

+
+ +
+
+ + + +
+ + +
+
+ + ); +}; + +interface IStepTwoProps { + onClose: () => void; + handleUpdateItem?: () => Promise; + 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 ( + <> + +

+ Update Item +

+

+ Apakah kamu yakin dengan +
perubahan yang dilakukan? +

+
+ + + + + + ); +}; + +export default ModalUpdateItem; diff --git a/apps/backoffice/src/app/gacha-roll/_hook/use-item.ts b/apps/backoffice/src/app/gacha-roll/_hook/use-item.ts new file mode 100644 index 0000000..1889359 --- /dev/null +++ b/apps/backoffice/src/app/gacha-roll/_hook/use-item.ts @@ -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({ + 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, + 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, + }; +}; diff --git a/apps/backoffice/src/app/gacha-roll/layout.tsx b/apps/backoffice/src/app/gacha-roll/layout.tsx new file mode 100644 index 0000000..a770c99 --- /dev/null +++ b/apps/backoffice/src/app/gacha-roll/layout.tsx @@ -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 ( +
+
+ +
+ +
+
+
+ ); +}; + +export default AppLayout; diff --git a/apps/backoffice/src/app/gacha-roll/page.tsx b/apps/backoffice/src/app/gacha-roll/page.tsx new file mode 100644 index 0000000..c57d204 --- /dev/null +++ b/apps/backoffice/src/app/gacha-roll/page.tsx @@ -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({ + pageIndex: 0, + pageSize: 9, + }); + + const [rowSelection, setRowSelection] = React.useState({}); + + const columns: ColumnDef[] = [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ), + }, + { + header: 'No', + accessorKey: 'id', + }, + { + header: 'Nama Item', + accessorKey: 'name', + }, + { + header: 'Chance Rate', + accessorKey: 'chanceRate', + }, + { + header: 'Quantity', + accessorKey: 'quantity', + }, + { + header: 'Action', + cell: () => ( +
+ + +
+ ), + }, + ]; + + 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 ( + +
+
+

Gacha Roll

+
+ +
+
+
+ +
+ +
+
+
+ +
+
+ + +
+
+ + setShowModalAddItem(false)} + nextStep={nextStep} + prevStep={prevStep} + resetStep={resetStep} + /> + setShowModalUpdateItem(false)} + nextStep={nextStep} + prevStep={prevStep} + resetStep={resetStep} + /> + setShowModalDeleteItem(false)} + handleDeleteItem={() => { + console.log('Item deleted'); + }} + /> +
+ ); +}; + +export default Components; diff --git a/apps/backoffice/src/app/page.tsx b/apps/backoffice/src/app/page.tsx index b92bf58..99b0c4e 100644 --- a/apps/backoffice/src/app/page.tsx +++ b/apps/backoffice/src/app/page.tsx @@ -1,10 +1,10 @@ import { FC, ReactElement } from 'react'; -import { useNavigate } from 'react-router'; import { Button } from '@imphnen-frontend-service/ui/atoms'; -import { InputForm } 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 (
@@ -13,21 +13,29 @@ export const Components: FC = (): ReactElement => {

Welcome to IMPHNEN Backoffice

- - - +
+ + + +
); diff --git a/apps/backoffice/src/app/permissions/_components/modal-add-permission.tsx b/apps/backoffice/src/app/permissions/_components/modal-add-permission.tsx new file mode 100644 index 0000000..a3b6243 --- /dev/null +++ b/apps/backoffice/src/app/permissions/_components/modal-add-permission.tsx @@ -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; + currentStep?: number; + nextStep: () => void; + prevStep: () => void; + resetStep: () => void; +} + +const ModalAddPermission = ({ + isOpen, + onClose, + currentStep, + nextStep, + resetStep, + handleAddItem, +}: IModalAddPermission) => { + return ( + { + onClose(); + resetStep(); + }} + disableEscapeKeyDown={true} + > + {currentStep === 1 && } + {currentStep === 2 && ( + + )} + + ); +}; + +interface IStepOneProps { + nextStep: () => void; + onClose: () => void; +} + +const StepOne = ({ nextStep }: IStepOneProps) => { + const { form, onSubmit } = useItem(nextStep); + + return ( + <> + +

+ Tambah Permissions +

+
+ +
+ + + + +
+ + ); +}; + +interface IStepTwoProps { + onClose: () => void; + handleAddItem?: () => Promise; + 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 ( + <> + +

+ Tambah Permissions +

+

+ Apakah kamu yakin ingin +
menambahkan permission ini? +

+
+ + + + + + ); +}; + +export default ModalAddPermission; diff --git a/apps/backoffice/src/app/permissions/_components/modal-delete-permission.tsx b/apps/backoffice/src/app/permissions/_components/modal-delete-permission.tsx new file mode 100644 index 0000000..ea96c09 --- /dev/null +++ b/apps/backoffice/src/app/permissions/_components/modal-delete-permission.tsx @@ -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; + 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 ( + + + Delete? +
+

+ Delete Permissions +

+

+ Apakah kamu yakin untuk menghapus permission ini? Menghapus data ini + mungkin akan mempengaruhi fungsional sistem +

+
+
+ + + + +
+ ); +}; + +export default ModalDeletePermission; diff --git a/apps/backoffice/src/app/permissions/_components/modal-update-permission.tsx b/apps/backoffice/src/app/permissions/_components/modal-update-permission.tsx new file mode 100644 index 0000000..985b125 --- /dev/null +++ b/apps/backoffice/src/app/permissions/_components/modal-update-permission.tsx @@ -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; + 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 ( + + +

+ Update Permissions +

+
+ + + + + +
+ ); +}; + +export default ModalUpdatePermission; diff --git a/apps/backoffice/src/app/permissions/_hook/use-item.ts b/apps/backoffice/src/app/permissions/_hook/use-item.ts new file mode 100644 index 0000000..7b9bcf0 --- /dev/null +++ b/apps/backoffice/src/app/permissions/_hook/use-item.ts @@ -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({ + // 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, + 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, + }; +}; diff --git a/apps/backoffice/src/app/permissions/layout.tsx b/apps/backoffice/src/app/permissions/layout.tsx new file mode 100644 index 0000000..a770c99 --- /dev/null +++ b/apps/backoffice/src/app/permissions/layout.tsx @@ -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 ( +
+
+ +
+ +
+
+
+ ); +}; + +export default AppLayout; diff --git a/apps/backoffice/src/app/permissions/page.tsx b/apps/backoffice/src/app/permissions/page.tsx new file mode 100644 index 0000000..95e7fb5 --- /dev/null +++ b/apps/backoffice/src/app/permissions/page.tsx @@ -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({ + pageIndex: 0, + pageSize: 9, + }); + + const [rowSelection, setRowSelection] = React.useState({}); + + const columns: ColumnDef[] = [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ), + }, + { + header: 'No', + accessorKey: 'id', + }, + { + header: 'Name', + accessorKey: 'name', + }, + { + header: 'Action', + cell: () => ( +
+ + +
+ ), + }, + ]; + + 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 ( + +
+
+

Permissions

+
+ +
+
+
+ +
+ +
+
+
+ +
+
+ + +
+
+ + setShowModalAddItem(false)} + nextStep={nextStep} + prevStep={prevStep} + resetStep={resetStep} + /> + setShowModalUpdateItem(false)} + nextStep={nextStep} + prevStep={prevStep} + resetStep={resetStep} + /> + setShowModalDeleteItem(false)} + nextStep={nextStep} + prevStep={prevStep} + resetStep={resetStep} + /> +
+ ); +}; + +export default Components; diff --git a/apps/backoffice/src/app/prizes/_components/modal-process-item.tsx b/apps/backoffice/src/app/prizes/_components/modal-process-item.tsx index 19a907b..a944663 100644 --- a/apps/backoffice/src/app/prizes/_components/modal-process-item.tsx +++ b/apps/backoffice/src/app/prizes/_components/modal-process-item.tsx @@ -1,5 +1,5 @@ import { Button } from '@imphnen-frontend-service/ui/atoms'; -import { InputForm, Modal } from '@imphnen-frontend-service/ui/molecules'; +import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules'; interface IModalProcessDelivery { isOpen: boolean; @@ -30,7 +30,7 @@ const ModalProcessDelivery = ({
- - - - void; + handleAdd?: () => Promise; + currentStep?: number; + nextStep: () => void; + prevStep: () => void; + resetStep: () => void; +} + +const ModalAddRole = ({ + isOpen, + onClose, + currentStep, + nextStep, + resetStep, + handleAdd, +}: IModalAddRole) => { + return ( + { + onClose(); + resetStep(); + }} + disableEscapeKeyDown={true} + > + {currentStep === 1 && } + {currentStep === 2 && ( + + )} + + ); +}; + +interface IStepOneProps { + nextStep: () => void; + onClose: () => void; +} + +const StepOne = ({ nextStep }: IStepOneProps) => { + const { form, onSubmit } = useItem(nextStep); + + return ( + <> + +

+ Tambah Roles +

+
+ +
+
+ +
+ +
+ + Permissions + +
+ {[ + 'Gacha Items', + 'Gacha Roll', + 'Roll', + 'Users', + 'Gacha Claim', + ].map((title) => ( +
+ {title} +
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ ))} +
+
+ + +
+
+ + ); +}; + +interface IStepTwoProps { + onClose: () => void; + handleAdd?: () => Promise; + 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 ( + <> + +

+ Tambah Roles +

+

+ Apakah kamu yakin ingin +
menambahkan role ini? +

+
+ + + + + + ); +}; + +export default ModalAddRole; diff --git a/apps/backoffice/src/app/roles/_components/modal-delete-role.tsx b/apps/backoffice/src/app/roles/_components/modal-delete-role.tsx new file mode 100644 index 0000000..68c45e9 --- /dev/null +++ b/apps/backoffice/src/app/roles/_components/modal-delete-role.tsx @@ -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; + 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 ( + + + Delete? +
+

+ Delete Roles +

+

+ Apakah kamu yakin untuk menghapus role ini? Menghapus data ini + mungkin akan mempengaruhi fungsional sistem +

+
+
+ + + + +
+ ); +}; + +export default ModalDeletePermission; diff --git a/apps/backoffice/src/app/roles/_components/modal-update-role.tsx b/apps/backoffice/src/app/roles/_components/modal-update-role.tsx new file mode 100644 index 0000000..f2605c7 --- /dev/null +++ b/apps/backoffice/src/app/roles/_components/modal-update-role.tsx @@ -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; + 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 ( + + +

+ Update Roles +

+
+ +
+ +
+ +
+ + Permissions + +
+ {['Gacha Items', 'Gacha Roll', 'Roll', 'Users', 'Gacha Claim'].map( + (title) => ( +
+ {title} +
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ ) + )} +
+
+ + +
+
+ ); +}; + +export default ModalUpdatePermission; diff --git a/apps/backoffice/src/app/roles/_hook/use-item.ts b/apps/backoffice/src/app/roles/_hook/use-item.ts new file mode 100644 index 0000000..7b9bcf0 --- /dev/null +++ b/apps/backoffice/src/app/roles/_hook/use-item.ts @@ -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({ + // 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, + 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, + }; +}; diff --git a/apps/backoffice/src/app/roles/layout.tsx b/apps/backoffice/src/app/roles/layout.tsx new file mode 100644 index 0000000..a770c99 --- /dev/null +++ b/apps/backoffice/src/app/roles/layout.tsx @@ -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 ( +
+
+ +
+ +
+
+
+ ); +}; + +export default AppLayout; diff --git a/apps/backoffice/src/app/roles/page.tsx b/apps/backoffice/src/app/roles/page.tsx new file mode 100644 index 0000000..d01581d --- /dev/null +++ b/apps/backoffice/src/app/roles/page.tsx @@ -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({ + pageIndex: 0, + pageSize: 9, + }); + + const [rowSelection, setRowSelection] = React.useState({}); + + const columns: ColumnDef[] = [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ), + }, + { + header: 'ID', + accessorKey: 'id', + }, + { + header: 'Roles Name', + accessorKey: 'name', + }, + { + header: 'Action', + cell: () => ( +
+ + +
+ ), + }, + ]; + + 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 ( + +
+
+

Roles

+
+ +
+
+
+ +
+ +
+
+
+ +
+
+ + +
+
+ + setShowModalAddItem(false)} + nextStep={nextStep} + prevStep={prevStep} + resetStep={resetStep} + /> + setShowModalUpdateItem(false)} + nextStep={nextStep} + prevStep={prevStep} + resetStep={resetStep} + /> + setShowModalDeleteItem(false)} + nextStep={nextStep} + prevStep={prevStep} + resetStep={resetStep} + /> +
+ ); +}; + +export default Components; diff --git a/apps/backoffice/src/app/transactions/_components/modal-validate.tsx b/apps/backoffice/src/app/transactions/_components/modal-validate.tsx index 5938490..11a2fdf 100644 --- a/apps/backoffice/src/app/transactions/_components/modal-validate.tsx +++ b/apps/backoffice/src/app/transactions/_components/modal-validate.tsx @@ -1,5 +1,5 @@ import { Button } from '@imphnen-frontend-service/ui/atoms'; -import { InputForm, Modal } from '@imphnen-frontend-service/ui/molecules'; +import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules'; interface IModalValidate { isOpen: boolean; @@ -27,7 +27,7 @@ const ModalValidate = ({ - + diff --git a/apps/backoffice/vite.config.ts b/apps/backoffice/vite.config.ts index 936ed86..260280d 100644 --- a/apps/backoffice/vite.config.ts +++ b/apps/backoffice/vite.config.ts @@ -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'])], diff --git a/apps/dimentorin/src/app/auth/login/page.tsx b/apps/dimentorin/src/app/auth/login/page.tsx index 98877d1..95440b3 100644 --- a/apps/dimentorin/src/app/auth/login/page.tsx +++ b/apps/dimentorin/src/app/auth/login/page.tsx @@ -1,45 +1,73 @@ import { FC, ReactElement } from 'react'; -import { LoginBanner } from "@imphnen-frontend-service/ui/organisms" +import { LoginBanner } from '@imphnen-frontend-service/ui/organisms'; import { Button } from '@imphnen-frontend-service/ui/atoms'; import { useSearchParams } from 'react-router-dom'; -import { InputForm } from '@imphnen-frontend-service/ui/molecules'; +import { InputField } from '@imphnen-frontend-service/ui/molecules'; export const Components: FC = (): ReactElement => { const [searchParams] = useSearchParams(); - const error = searchParams.get("error"); // Ambil nilai ?error= + const error = searchParams.get('error'); // Ambil nilai ?error= return ( -
-
+
+
-
-
-

Hallo Minna-san

-
Welcome to Dimentorin by IMPHNEN
- - -
- Lupa Password ? +
+
+

+ Hallo Minna-san +

+
+ Welcome to Dimentorin by IMPHNEN +
+ + + - -
+ +
Belum Punya akun ?
- Daftar Disini + + Daftar Disini +
Or
- -
+
); }; -export default Components; \ No newline at end of file +export default Components; diff --git a/apps/dimentorin/src/app/auth/register/page.tsx b/apps/dimentorin/src/app/auth/register/page.tsx index a58d433..a3933ce 100644 --- a/apps/dimentorin/src/app/auth/register/page.tsx +++ b/apps/dimentorin/src/app/auth/register/page.tsx @@ -1,44 +1,102 @@ 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 { InputForm } from '@imphnen-frontend-service/ui/molecules'; +import { InputField } from '@imphnen-frontend-service/ui/molecules'; export const Components: FC = (): ReactElement => { return ( -
-
+
+
-
-
-

Register

-
Yosha~! Saatnya Bergabung dengan Dimentorin
-
- - - -
-
-

OTP Code

- +
+
+

+ Register +

+
+ Yosha~! Saatnya Bergabung dengan Dimentorin +
+
+ + + +
+
+

OTP Code

+
-
- +
+
- -
"Senpai~! Pastikan password-mu sekuat pertahanan kastil!"
-
Tips membuat password yang OP:
-
- Minimal 8 karakter (semakin panjang, semakin power-up! )
-
- Campur huruf besar, kecil, angka, dan simbol untuk kombinasi ultimate!🔥
-
- Jangan pakai password yang gampang ditebak, nanti ketahuan musuh!🚨
- - -
+ +
+ "Senpai~! Pastikan password-mu sekuat pertahanan kastil!" +
+
+ Tips membuat password yang OP: +
+
+ - Minimal 8 karakter (semakin panjang, semakin power-up!{' '} + + ⚡ + + ) +
+
+ - Campur huruf besar, kecil, angka, dan simbol untuk kombinasi + ultimate! + + 🔥 + +
+
+ - Jangan pakai password yang gampang ditebak, nanti ketahuan + musuh! + + 🚨 + +
+ + +
); }; -export default Components; \ No newline at end of file +export default Components; diff --git a/apps/dimentorin/src/app/auth/register/success/page.tsx b/apps/dimentorin/src/app/auth/register/success/page.tsx index a2d1652..b648e4b 100644 --- a/apps/dimentorin/src/app/auth/register/success/page.tsx +++ b/apps/dimentorin/src/app/auth/register/success/page.tsx @@ -1,33 +1,55 @@ import { FC, ReactElement } from 'react'; -import { RegisterResetBanner } from "@imphnen-frontend-service/ui/organisms"; -import { Button, Input } from '@imphnen-frontend-service/ui/atoms'; -import { InputForm } from '@imphnen-frontend-service/ui/molecules'; +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 ( -
-
+
+
-
-
-

Register Successfull

-

Yosha~! Saatnya Bergabung dengan Dimentorin!

- - - - +
+
+

+ Register Successfull +

+

+ Yosha~! Saatnya Bergabung dengan Dimentorin! +

+ + + + -

Kamu akan memasuki isekai dalam 10 dtk

- -
+
); }; -export default Components; \ No newline at end of file +export default Components; diff --git a/apps/gacha/src/app/_components/form/modal-form-forgot-password.tsx b/apps/gacha/src/app/_components/form/modal-form-forgot-password.tsx index 4f42d91..9d0c14e 100644 --- a/apps/gacha/src/app/_components/form/modal-form-forgot-password.tsx +++ b/apps/gacha/src/app/_components/form/modal-form-forgot-password.tsx @@ -1,11 +1,11 @@ import { Button } from '@imphnen-frontend-service/ui/atoms'; import { - InputForm, + InputField, Modal, Stepper, } from '@imphnen-frontend-service/ui/molecules'; -import { useQueryState } from '../../../hooks/use-query-state'; - +import { useQueryState } from '@imphnen-frontend-service/utils'; +import { Fragment } from 'react/jsx-runtime'; interface IModalFormForgotPasswordProps { isOpen: boolean; onClose: () => void; @@ -62,7 +62,7 @@ interface IStepOneProps { const StepOne = ({ nextStep, onClose }: IStepOneProps) => ( <> - ( - <> - {/* TODO: Change component using OTP Input */} - + ( Reset Password
- + ); interface IStepThreeProps { @@ -118,14 +117,14 @@ interface IStepThreeProps { const StepThree = ({ onClose, resetStep }: IStepThreeProps) => ( <> - - void; onForgotPassword: () => void; + setIsOpenRegisterModal: (value: boolean) => void; } const ModalFormLogin = ({ isOpen, onClose, onForgotPassword, + setIsOpenRegisterModal, }: IModalFormLogin) => { + const { form, onSubmit } = useLogin(); + return ( - - - -

- +

+ - - -
-

Belum punya akun?

- - Daftar - -
+
+

Belum punya akun?

+ +
+
); diff --git a/apps/gacha/src/app/_components/form/modal-form-register.tsx b/apps/gacha/src/app/_components/form/modal-form-register.tsx index 3873c1d..5eb5c82 100644 --- a/apps/gacha/src/app/_components/form/modal-form-register.tsx +++ b/apps/gacha/src/app/_components/form/modal-form-register.tsx @@ -1,6 +1,11 @@ import { Button } from '@imphnen-frontend-service/ui/atoms'; -import { InputForm, Modal } from '@imphnen-frontend-service/ui/molecules'; -import { useQueryState } from '../../../hooks/use-query-state'; +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 ( { {currentStep === 1 ? 'Info Akun' : 'Informasi Pengiriman Hadiah'} - - {currentStep === 1 && } - {currentStep === 2 && ( - - )} + +
+ {currentStep === 1 && ( + + )} + {currentStep === 2 && } +
); }; interface IStepOneProps { + form: UseFormReturn; nextStep: () => void; - onClose: () => void; + isStepOneValid: boolean; } -const StepOne = ({ nextStep, onClose }: IStepOneProps) => ( - <> - - - - - - -); +const StepOne = ({ form, nextStep, isStepOneValid }: IStepOneProps) => { + return ( + <> + + + + + + + ); +}; interface IStepTwoProps { - nextStep: () => void; + form: UseFormReturn; prevStep: () => void; } -const StepTwo = ({ nextStep, prevStep }: IStepTwoProps) => ( +const StepTwo = ({ form, prevStep }: IStepTwoProps) => ( <> - - + + ( -
diff --git a/apps/gacha/src/app/_components/item/gacha-item.tsx b/apps/gacha/src/app/_components/item/gacha-item.tsx new file mode 100644 index 0000000..df14481 --- /dev/null +++ b/apps/gacha/src/app/_components/item/gacha-item.tsx @@ -0,0 +1,29 @@ +import { cn } from '@imphnen-frontend-service/utils'; +import { FC, ReactElement } from 'react'; + +type TGachaItem = { + src: string; + label: string; + className?: string; +}; + +export const GachaItem: FC = ({ + src, + label, + className, +}): ReactElement => { + return ( +
+
+ Banner +
+

{label}

+
+ ); +}; diff --git a/apps/gacha/src/app/_hooks/use-login.ts b/apps/gacha/src/app/_hooks/use-login.ts new file mode 100644 index 0000000..272b2ba --- /dev/null +++ b/apps/gacha/src/app/_hooks/use-login.ts @@ -0,0 +1,26 @@ +import { useForm } from 'react-hook-form'; +import { + authLoginSchema, + TLoginRequest, + usePostLogin, +} from '@imphnen-frontend-service/service'; +import { zodResolver } from '@hookform/resolvers/zod'; + +export const useLogin = () => { + const postLogin = usePostLogin(); + const form = useForm({ + resolver: zodResolver(authLoginSchema), + mode: 'all', + }); + + const onSubmit = form.handleSubmit((data) => { + postLogin.mutate(data, { + onSuccess: () => console.log('Success Login'), + }); + }); + + return { + form, + onSubmit, + }; +}; diff --git a/apps/gacha/src/app/_hooks/use-register.ts b/apps/gacha/src/app/_hooks/use-register.ts new file mode 100644 index 0000000..5817c30 --- /dev/null +++ b/apps/gacha/src/app/_hooks/use-register.ts @@ -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({ + 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, + }; +}; diff --git a/apps/gacha/src/app/layout.tsx b/apps/gacha/src/app/layout.tsx index 7428943..2b27454 100644 --- a/apps/gacha/src/app/layout.tsx +++ b/apps/gacha/src/app/layout.tsx @@ -1,14 +1,16 @@ -import { FC, ReactElement } from 'react'; -import { Outlet } from 'react-router-dom'; import { Navbar } from '@imphnen-frontend-service/ui/organisms'; +import { Outlet } from 'react-router-dom'; +import { FC, ReactElement } from 'react'; +import { ModalLoginProvider } from '@imphnen-frontend-service/utils'; export const AppLayout: FC = (): ReactElement => { return ( -
- - -
+ +
+ + +
+
); }; - export default AppLayout; diff --git a/apps/gacha/src/app/page.tsx b/apps/gacha/src/app/page.tsx index 1d2ad4f..c9cf71d 100644 --- a/apps/gacha/src/app/page.tsx +++ b/apps/gacha/src/app/page.tsx @@ -1,20 +1,15 @@ import { ArrowDownOutlined } from '@ant-design/icons'; import { Button } from '@imphnen-frontend-service/ui/atoms'; -import { cn } from '@imphnen-frontend-service/utils'; -import { FC, Fragment, ReactElement, useEffect, useState } from 'react'; +import { FC, Fragment, ReactElement, useState } from 'react'; import ModalFormForgotPassword from './_components/form/modal-form-forgot-password'; import ModalFormLogin from './_components/form/modal-form-login'; import ModalFormRegister from './_components/form/modal-form-register'; - -interface GachaItemProps { - src: string; - label: string; - className?: string; -} +import { GachaItem } from './_components/item/gacha-item'; +import { useModalLogin } from '@imphnen-frontend-service/utils'; export const Components: FC = (): ReactElement => { + const { showModalLogin, setShowModalLogin } = useModalLogin(); const [showModalForgotPassword, setShowModalForgotPassword] = useState(false); - const [showModalLogin, setShowModalLogin] = useState(false); const [showModalRegister, setShowModalRegister] = useState(false); const scrollToRoulette = () => { @@ -24,49 +19,6 @@ export const Components: FC = (): ReactElement => { } }; - useEffect(() => { - const gachaPlaySection = document.getElementById('gacha-play'); - if (gachaPlaySection) { - let currentIndex = 0; - const items = gachaPlaySection.children; - const totalItems = items.length; - - const scrollItems = () => { - if (currentIndex >= totalItems) { - currentIndex = 0; - } - items[currentIndex].scrollIntoView({ - behavior: 'smooth', - inline: 'center', - }); - currentIndex++; - }; - - const intervalId = setInterval(scrollItems, 2800); - - return () => clearInterval(intervalId); - } - }, []); - - const GachaItem: FC = ({ - src, - label, - className, - }): ReactElement => { - return ( -
-
- -
-

{label}

-
- ); - }; - const handleForgotPasswordClick = () => { setShowModalLogin(false); setShowModalForgotPassword(true); @@ -74,7 +26,6 @@ export const Components: FC = (): ReactElement => { return ( - {/* Landing Page */}
{ ~ 175k ~
-
+
Merch 2
@@ -163,7 +120,7 @@ export const Components: FC = (): ReactElement => {
- {/* Roulette Page */} +
{
-
+
Here Take Your Prize
@@ -223,31 +177,17 @@ export const Components: FC = (): ReactElement => { Spin Now
- {/* TODO: Change using real button trigger */} -
- - - -
- {/* Diffuser & Cloud */}
- {/* Login Modal */} setShowModalLogin(false)} onForgotPassword={handleForgotPasswordClick} + setIsOpenRegisterModal={setShowModalRegister} key="login" /> - {/* Register Modal */} { @@ -256,7 +196,6 @@ export const Components: FC = (): ReactElement => { key="register" /> - {/* Forgot Password Modal */} { diff --git a/apps/gacha/src/main.tsx b/apps/gacha/src/main.tsx index 7201952..919d102 100644 --- a/apps/gacha/src/main.tsx +++ b/apps/gacha/src/main.tsx @@ -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( + diff --git a/apps/landing/.npmrc b/apps/landing/.npmrc new file mode 100644 index 0000000..cf04042 --- /dev/null +++ b/apps/landing/.npmrc @@ -0,0 +1,2 @@ +shamefully-hoist=true +strict-peer-dependencies=false diff --git a/apps/landing/eslint.config.mjs b/apps/landing/eslint.config.mjs new file mode 100644 index 0000000..05e4cb7 --- /dev/null +++ b/apps/landing/eslint.config.mjs @@ -0,0 +1,30 @@ +import { FlatCompat } from '@eslint/eslintrc'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import js from '@eslint/js'; +import baseConfig from '../../eslint.config.mjs'; +const compat = new FlatCompat({ + baseDirectory: dirname(fileURLToPath(import.meta.url)), + recommendedConfig: js.configs.recommended, +}); + +export default [ + ...baseConfig, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'], + // Override or add rules here + rules: {}, + }, + ...compat.extends('@nuxt/eslint-config'), + { + files: ['**/*.vue'], + languageOptions: { + parserOptions: { + parser: await import('@typescript-eslint/parser'), + }, + }, + }, + { + ignores: ['.nuxt/**', '.output/**', 'node_modules'], + }, +]; diff --git a/apps/landing/nuxt.config.ts b/apps/landing/nuxt.config.ts new file mode 100644 index 0000000..6464ecd --- /dev/null +++ b/apps/landing/nuxt.config.ts @@ -0,0 +1,25 @@ +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { defineNuxtConfig } from 'nuxt/config'; + +// https://nuxt.com/docs/api/configuration/nuxt-config +export default defineNuxtConfig({ + workspaceDir: '../../', + srcDir: 'src', + devtools: { enabled: true }, + devServer: { + host: 'localhost', + port: 3000, + }, + typescript: { + typeCheck: true, + tsConfig: { + extends: '../tsconfig.app.json', // Nuxt copies this string as-is to the `./.nuxt/tsconfig.json`, therefore it needs to be relative to that directory + }, + }, + imports: { + autoImport: true, + }, + vite: { + plugins: [nxViteTsPaths()], + }, +}); diff --git a/apps/landing/project.json b/apps/landing/project.json new file mode 100644 index 0000000..97f5751 --- /dev/null +++ b/apps/landing/project.json @@ -0,0 +1,8 @@ +{ + "name": "landing", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "application", + "sourceRoot": "apps/landing/src", + "// targets": "to see all targets run: nx show project landing --web", + "targets": {} +} diff --git a/apps/landing/src/app.vue b/apps/landing/src/app.vue new file mode 100644 index 0000000..d96c753 --- /dev/null +++ b/apps/landing/src/app.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/apps/landing/src/components/NxWelcome.vue b/apps/landing/src/components/NxWelcome.vue new file mode 100644 index 0000000..d9b62ac --- /dev/null +++ b/apps/landing/src/components/NxWelcome.vue @@ -0,0 +1,814 @@ + + + + + diff --git a/apps/landing/src/pages/about.vue b/apps/landing/src/pages/about.vue new file mode 100644 index 0000000..e5f1bc9 --- /dev/null +++ b/apps/landing/src/pages/about.vue @@ -0,0 +1,16 @@ + + + diff --git a/apps/landing/src/pages/index.vue b/apps/landing/src/pages/index.vue new file mode 100644 index 0000000..675eb0f --- /dev/null +++ b/apps/landing/src/pages/index.vue @@ -0,0 +1,3 @@ + diff --git a/apps/landing/src/public/.gitkeep b/apps/landing/src/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/landing/src/public/favicon.ico b/apps/landing/src/public/favicon.ico new file mode 100644 index 0000000..3146711 Binary files /dev/null and b/apps/landing/src/public/favicon.ico differ diff --git a/apps/landing/src/server/api/greet.ts b/apps/landing/src/server/api/greet.ts new file mode 100644 index 0000000..9109551 --- /dev/null +++ b/apps/landing/src/server/api/greet.ts @@ -0,0 +1,10 @@ +import { defineEventHandler, getQuery } from 'h3'; + +export default defineEventHandler((event) => { + const q = getQuery(event); + const projectName = q.name || 'World'; + + return { + message: `Hello ${projectName}`, + }; +}); diff --git a/apps/landing/src/server/tsconfig.json b/apps/landing/src/server/tsconfig.json new file mode 100644 index 0000000..3c6e968 --- /dev/null +++ b/apps/landing/src/server/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../.nuxt/tsconfig.server.json" +} diff --git a/apps/landing/tsconfig.app.json b/apps/landing/tsconfig.app.json new file mode 100644 index 0000000..c3a330d --- /dev/null +++ b/apps/landing/tsconfig.app.json @@ -0,0 +1,21 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "composite": true + }, + "include": [".nuxt/nuxt.d.ts", "src/**/*"], + "exclude": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx" + ] +} diff --git a/apps/landing/tsconfig.json b/apps/landing/tsconfig.json new file mode 100644 index 0000000..dc6be23 --- /dev/null +++ b/apps/landing/tsconfig.json @@ -0,0 +1,13 @@ +{ + "files": [], + "include": [".nuxt/nuxt.d.ts"], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../tsconfig.base.json" +} diff --git a/apps/landing/tsconfig.spec.json b/apps/landing/tsconfig.spec.json new file mode 100644 index 0000000..1436c0a --- /dev/null +++ b/apps/landing/tsconfig.spec.json @@ -0,0 +1,30 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "composite": true + }, + "include": [ + ".nuxt/nuxt.d.ts", + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/apps/landing/vitest.config.ts b/apps/landing/vitest.config.ts new file mode 100644 index 0000000..68561bc --- /dev/null +++ b/apps/landing/vitest.config.ts @@ -0,0 +1,26 @@ +/// +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/landing', + plugins: [vue(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + test: { + watch: false, + globals: true, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/landing', + provider: 'v8' as const, + }, + }, +})); diff --git a/docs/logo.svg b/docs/logo.svg new file mode 100644 index 0000000..ca0f36e --- /dev/null +++ b/docs/logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/eslint.config.mjs b/eslint.config.mjs index ae5c194..bef74af 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -12,7 +12,7 @@ export default [ ], }, { - files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'], rules: { '@nx/enforce-module-boundaries': [ 'error', diff --git a/libs/service/src/api/auth/index.ts b/libs/service/src/api/auth/index.ts index e925dd1..0241638 100644 --- a/libs/service/src/api/auth/index.ts +++ b/libs/service/src/api/auth/index.ts @@ -1,4 +1,4 @@ -import { api } from '@imphnen-frontend-service/utils'; +import { api } from '../'; import { TLoginRequest, TLoginResponse, diff --git a/libs/service/src/api/index.ts b/libs/service/src/api/index.ts index 76d0cf2..7745a7c 100644 --- a/libs/service/src/api/index.ts +++ b/libs/service/src/api/index.ts @@ -1,3 +1,11 @@ +import axios, { AxiosRequestConfig } from 'axios'; + export * from './auth'; export * from './gacha'; export * from './users'; + +const config: AxiosRequestConfig = { + baseURL: import.meta.env.VITE_API_URL, +}; + +export const api = axios.create(config); diff --git a/libs/service/src/hooks/auth/index.ts b/libs/service/src/hooks/auth/index.ts index dda436e..d4fd99e 100644 --- a/libs/service/src/hooks/auth/index.ts +++ b/libs/service/src/hooks/auth/index.ts @@ -6,6 +6,8 @@ import { TRegisterRequest, TVerifyEmailRequest, } from '../../types/auth'; +import { SessionToken, SessionUser } from '@imphnen-frontend-service/utils'; + import { TResponseError, TResponseMessage } from '../../types/common'; export const usePostLogin = (): UseMutationResult< @@ -17,6 +19,11 @@ export const usePostLogin = (): UseMutationResult< return useMutation({ mutationKey: ['post-login'], mutationFn: async (payload) => await postLogin(payload), + onSuccess: (res) => { + SessionUser.set(res.data.user); + SessionToken.set(res.data.token); + window.location.reload(); + }, }); }; diff --git a/libs/service/src/index.ts b/libs/service/src/index.ts index 6c2f44a..5314ca1 100644 --- a/libs/service/src/index.ts +++ b/libs/service/src/index.ts @@ -1,3 +1,4 @@ export * from './api'; export * from './hooks'; export * from './types'; +export * from './schemas'; diff --git a/libs/service/src/schemas/auth/index.ts b/libs/service/src/schemas/auth/index.ts new file mode 100644 index 0000000..d406f97 --- /dev/null +++ b/libs/service/src/schemas/auth/index.ts @@ -0,0 +1,78 @@ +import { z } from 'zod'; + +export const authLoginSchema = 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'), + password: z + .string({ + required_error: 'Password tidak boleh kosong', + invalid_type_error: 'Password harus berupa string', + }) + .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 +); diff --git a/libs/service/src/schemas/gacha/index.ts b/libs/service/src/schemas/gacha/index.ts new file mode 100644 index 0000000..765037d --- /dev/null +++ b/libs/service/src/schemas/gacha/index.ts @@ -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'), +}); diff --git a/libs/service/src/schemas/index.ts b/libs/service/src/schemas/index.ts new file mode 100644 index 0000000..94e94e5 --- /dev/null +++ b/libs/service/src/schemas/index.ts @@ -0,0 +1,2 @@ +export * from './auth'; +export * from './gacha'; diff --git a/libs/service/src/types/auth/index.ts b/libs/service/src/types/auth/index.ts index bf094ec..8bd2d8d 100644 --- a/libs/service/src/types/auth/index.ts +++ b/libs/service/src/types/auth/index.ts @@ -1,3 +1,5 @@ +import { TUserItem } from '../users'; + export type TLoginRequest = { email: string; password: string; @@ -9,18 +11,19 @@ export type TLoginResponse = { access_token: string; refresh_token: string; }; - user: { - fullname: string; - email: string; - is_active: boolean; - }; + user: TUserItem; }; }; 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 = { diff --git a/libs/service/src/types/gacha/index.ts b/libs/service/src/types/gacha/index.ts index cb0ff5c..14a1415 100644 --- a/libs/service/src/types/gacha/index.ts +++ b/libs/service/src/types/gacha/index.ts @@ -1 +1,11 @@ -export {}; +export type TGachaItem = { + itemName: string; + quantity: number; + foto?: File; +}; + +export type TGachaRollItem = { + itemName: string; + quantity: number; + chanceRate: number; +}; diff --git a/libs/service/src/types/index.ts b/libs/service/src/types/index.ts index 76d0cf2..5256155 100644 --- a/libs/service/src/types/index.ts +++ b/libs/service/src/types/index.ts @@ -1,3 +1,5 @@ export * from './auth'; export * from './gacha'; export * from './users'; +export * from './roles'; +export * from './permissions'; diff --git a/libs/service/src/types/permissions/index.ts b/libs/service/src/types/permissions/index.ts new file mode 100644 index 0000000..66e4d7f --- /dev/null +++ b/libs/service/src/types/permissions/index.ts @@ -0,0 +1,6 @@ +export type TPermissionItem = { + id: string; + name: string; + created_at: string; + updated_at: string; +}; diff --git a/libs/service/src/types/roles/index.ts b/libs/service/src/types/roles/index.ts new file mode 100644 index 0000000..9a51667 --- /dev/null +++ b/libs/service/src/types/roles/index.ts @@ -0,0 +1,9 @@ +import { TPermissionItem } from '../permissions'; + +export type TRoleItem = { + id: string; + name: string; + created_at: string; + updated_at: string; + permissions: TPermissionItem[]; +}; diff --git a/libs/service/src/types/users/index.ts b/libs/service/src/types/users/index.ts index cb0ff5c..8b6abae 100644 --- a/libs/service/src/types/users/index.ts +++ b/libs/service/src/types/users/index.ts @@ -1 +1,19 @@ -export {}; +import { TRoleItem } from '../roles'; + +export type TUserItem = { + id: string; + avatar: string; + birthdate: string; + email: string; + fullname: string; + gender: string; + identity_number: string; + is_active: boolean; + is_profile_completed: boolean; + phone_number: string; + referral_code: string; + referred_by: string; + religion: string; + student_type: string; + role: TRoleItem; +}; diff --git a/libs/ui/src/atoms/button/button.spec.tsx b/libs/ui/src/atoms/button/button.spec.tsx index d6c2dd3..bb74faa 100644 --- a/libs/ui/src/atoms/button/button.spec.tsx +++ b/libs/ui/src/atoms/button/button.spec.tsx @@ -24,9 +24,9 @@ describe('Test Button Component', () => { const button = screen.getByText('Delete'); - expect(button).toHaveClass('bg-danger-500'); - expect(button).toHaveClass('hover:bg-danger-600'); - expect(button).toHaveClass('text-white'); + expect(button).toHaveClass('bg-danger-100'); + expect(button).toHaveClass('hover:bg-danger-200'); + expect(button).toHaveClass('text-danger-500'); }); it("disables the button when 'disabled' prop is set", async () => { diff --git a/libs/ui/src/atoms/button/button.tsx b/libs/ui/src/atoms/button/button.tsx index dc831b5..20c1677 100644 --- a/libs/ui/src/atoms/button/button.tsx +++ b/libs/ui/src/atoms/button/button.tsx @@ -31,7 +31,7 @@ const variantClasses: Record = { bordered: 'border border-primary-500 hover:border-primary-600 bg-transparent hover:text-primary-600 hover:bg-gray-50 text-primary-500', success: 'bg-success-500 hover:bg-success-600 text-white shadow-md', - danger: 'bg-danger-500 hover:bg-danger-600 text-white shadow-md', + danger: 'bg-danger-100 hover:bg-danger-200 text-danger-500 shadow-md', }; const sizeClasses: Record = { diff --git a/libs/ui/src/atoms/input/input.tsx b/libs/ui/src/atoms/input/input.tsx index 427b4fc..664169a 100644 --- a/libs/ui/src/atoms/input/input.tsx +++ b/libs/ui/src/atoms/input/input.tsx @@ -9,7 +9,7 @@ import { EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons'; // Import import { cn } from '@imphnen-frontend-service/utils'; import { Button } from '../button'; -type TInputType = 'text' | 'email' | 'password' | 'file'; +type TInputType = 'text' | 'email' | 'number' | 'password' | 'file'; type TInputSize = 'sm' | 'md' | 'lg'; type Width = 'standard' | 'custom' @@ -43,7 +43,8 @@ export const Input: FC = ({ }): ReactElement => { const [showPassword, setShowPassword] = useState(false); // State for password visibility - const togglePasswordVisibility = () => { + const togglePasswordVisibility = (e: React.FormEvent) => { + e.preventDefault(); if (!disabled) setShowPassword((prev) => !prev); }; @@ -66,6 +67,7 @@ export const Input: FC = ({ {type === 'password' && (
); }; - -export default InputForm; diff --git a/libs/ui/src/molecules/input-form/index.ts b/libs/ui/src/molecules/input-form/index.ts deleted file mode 100644 index 14dcfcb..0000000 --- a/libs/ui/src/molecules/input-form/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './input-form'; diff --git a/libs/ui/src/organisms/backoffice-sidebar/backoffice-sidebar.tsx b/libs/ui/src/organisms/backoffice-sidebar/backoffice-sidebar.tsx index c8b77ec..676dce0 100644 --- a/libs/ui/src/organisms/backoffice-sidebar/backoffice-sidebar.tsx +++ b/libs/ui/src/organisms/backoffice-sidebar/backoffice-sidebar.tsx @@ -3,24 +3,26 @@ import { AuditOutlined, InboxOutlined, LogoutOutlined, + ReloadOutlined, + UsergroupAddOutlined, UserOutlined, + UserSwitchOutlined, } from '@ant-design/icons'; import { Button } from '../../atoms'; import { FC, ReactElement } from 'react'; -import { Link, useLocation, useNavigate } from 'react-router-dom'; +import { Link, useLocation } from 'react-router-dom'; +import { useSession } from '@imphnen-frontend-service/utils'; export const BackofficeSidebar: FC = (): ReactElement => { + const { signOut } = useSession(); const location = useLocation(); - const navigate = useNavigate(); const isActive = (path: string) => location.pathname.includes(path); return (