Compare commits

...
Author SHA1 Message Date
egagofur 638b64b715 fix: validation by query param 2025-04-04 16:42:27 +07:00
egagofur d61cb4a215 fix: split zod validation register modal 2025-04-04 16:38:21 +07:00
Maulana SodiqinandGitHub 272db82c8c Merge pull request #18 from IMPHNEN/feat/register-integeration
feat: integeration register modal
2025-04-04 06:03:13 +07:00
egagofur 9108ddb742 fix: unused field 2025-04-03 23:39:41 +07:00
egagofur 628e168342 fix: form type 2025-04-03 23:39:09 +07:00
egagofur 9aff9e2dc8 feat: integeration register modal 2025-04-03 23:32:44 +07:00
Maulana SodiqinandGitHub 585ad482f2 Merge pull request #16 from IMPHNEN/feat/backoffice-login
Backoffice login
2025-04-03 23:02:36 +08:00
Hafid Nur 988476b409 refactor auth and logout
- Pindah hard-code API version ke environment variable
- Gunakan helper untuk logout
2025-04-03 21:59:31 +07:00
Hafid Nur 38e6d4643b Merge branch 'develop' into feat/backoffice-login 2025-04-03 21:51:00 +07:00
Maulana SodiqinandGitHub 79cb9d81fc Merge pull request #15 from IMPHNEN/docs
Update README
2025-04-03 22:25:19 +08:00
Maulana SodiqinandGitHub 64e702df5d Merge pull request #17 from IMPHNEN/feat/handle-state-modal
Feat/handle state modal
2025-04-03 21:01:05 +08:00
egagofur 280afd68ab fix: template github 2025-04-03 19:51:31 +07:00
egagofur 9777b7bea9 feat: add toast library 2025-04-03 19:40:19 +07:00
egagofur 6bad504d44 feat: change wording validation 2025-04-03 19:40:04 +07:00
egagofur 6b959a2f22 feat: change from dummy button to navbar button 2025-04-03 19:39:53 +07:00
Hafid Nur 33c04707f8 feat: login backoffice 2025-04-03 16:28:54 +07:00
Hafid Nur f87dc4c782 Merge branch 'develop' into feat/backoffice-login 2025-04-03 15:45:23 +07:00
Hafid NurandGitHub b5428d8552 Update README.md 2025-04-03 14:15:52 +07:00
Hafid Nur 0a846b0512 docs: fix 2025-04-03 14:03:38 +07:00
Hafid Nur 3a950ea867 docs: align center logo 2025-04-03 14:01:14 +07:00
Hafid Nur 4693284b37 docs: Update README 2025-04-03 13:59:27 +07:00
Maulana Sodiqin f8f796e071 chore: fix global error build 2025-04-03 12:31:16 +07:00
Hafid Nur fe1928e8ef Merge branch 'develop' into feat/backoffice-login 2025-04-03 12:29:28 +07:00
Maulana SodiqinandGitHub 9c7b8c1c63 chore: kesalahan pemula 2025-04-03 12:27:22 +07:00
Maulana SodiqinandGitHub 2bd767d20b chore: kesalahan pemula 2025-04-03 12:26:45 +07:00
Maulana Sodiqin a6fea630a4 feat: login example 2025-04-03 12:21:10 +07:00
Maulana Sodiqin c05061becf feat: login example 2025-04-03 12:21:08 +07:00
Hafid Nur a9cc728ade feat(backoffice): implement login and logout
- Tambah fitur login dan logout pada Backoffice
- Ubah port development server dan atasi CORS
- Update type login response
- Update component Input agar toggle password berfungsi dengan benar
2025-04-03 06:10:07 +07:00
Hafid Nur 2602657201 feat(backoffice): send login request
- buat .env untuk menyimpan API dan memberikan .env.example juga untuk contohnya. Tambah .gitignore juga
- tambah fungsi login pada Backoffice
2025-04-02 22:42:02 +07:00
Maulana SodiqinandGitHub c70e5706bc Merge pull request #13 from IMPHNEN/feat/register-form
Feat/register form
2025-04-01 21:16:59 +08:00
Maulana SodiqinandGitHub 49ca2a9fb0 Merge pull request #12 from IMPHNEN/feat/backoffice-modal
Slicing the design of the Modal component in Backoffice
2025-04-01 21:10:53 +08:00
egagofur 47ebd3ad73 feat: add mr template 2025-04-01 10:17:42 +07:00
egagofur d544e43abc feat: add register form and change hooks to modal component 2025-04-01 10:14:46 +07:00
Hafid Nur d7a90f47d0 feat: dynamic radio button for Filter component 2025-03-31 22:46:09 +07:00
Hafid Nur 984a31c587 fix: create custom hook and stepper to make multi-step modal functional
- Tambah custom hook 'use-query-state' dan implementasi stepper pada modal dengan tahapan lebih dari 1
- Hapus component ModalBackoffice karena sudah menggunakan component umum Modal
2025-03-31 22:36:37 +07:00
Hafid Nur a575e534b5 feat: slicing modal for backoffice 'data akun' page 2025-03-31 21:53:18 +07:00
Hafid Nur 989503f930 feat: slicing modal for backoffice 'validasi transaksi' page
- Tambah fungsi untuk handle aksi ketika button pada modal ditekan
2025-03-31 21:40:00 +07:00
Hafid Nur 3b979119ed feat: slicing modal for backoffice 'data pengiriman hadiah' page 2025-03-31 17:32:30 +07:00
Hafid Nur ecec423d30 feat: slicing modal for backoffice dashboard page 2025-03-31 01:33:18 +07:00
Hafid Nur 3bbdba7015 fix: add type=file on Input & InputForm components
Juga fix script untuk serve backoffice
2025-03-31 01:13:21 +07:00
Hafid Nur f7a7acaf65 feat(backoffice): add modal delete gacha item 2025-03-31 00:30:05 +07:00
Maulana SodiqinandGitHub 809eeee931 Merge pull request #11 from IMPHNEN/feat/forgot-password
feat: dyamic stepper form forgot password
2025-03-30 23:49:15 +08:00
egagofur f39466936f feat: add todo change using real button trigger 2025-03-30 22:47:29 +07:00
egagofur 5d418830b9 feat: add todo component otp input 2025-03-30 22:44:59 +07:00
egagofur d58ad58ef8 feat: dyamic stepper form forgot password 2025-03-30 22:41:06 +07:00
Maulana SodiqinandGitHub b62c762cf9 Merge pull request #7 from IMPHNEN/DimentorinAuth
feat: Auth
2025-03-29 22:19:13 +08:00
Naufal AzmiandGitHub 4e665cca76 Merge branch 'develop' into DimentorinAuth 2025-03-29 21:00:10 +07:00
boboiazumi 74c0ebc5dd fix: bugs 2025-03-29 20:57:48 +07:00
boboiazumi eadd554a32 fix: conflict 2025-03-29 20:47:38 +07:00
boboiazumi ecdaa4a82e feat: Forgot Password Slicing, fix: webp image 2025-03-29 20:42:54 +07:00
Maulana SodiqinandGitHub 747e75e684 Merge pull request #8 from IMPHNEN/feat/backoffice
Backoffice Update
2025-03-29 21:18:10 +08:00
Maulana SodiqinandGitHub 76637dd051 Merge pull request #10 from IMPHNEN/feat/modal-unit-test-and-storybook
Feat/modal unit test and storybook
2025-03-29 21:16:51 +08:00
egagofur 3dda8149ba Merge branch 'develop' into feat/modal-unit-test-and-storybook 2025-03-29 17:56:39 +07:00
egagofur cc21735468 feat: unit test and storybook modal 2025-03-29 17:56:18 +07:00
Maulana SodiqinandGitHub a6bc6de72e Merge pull request #9 from IMPHNEN/feat/gacha-modal-login
feat: add modal login gacha page
2025-03-29 18:33:12 +08:00
egagofur 340bd8c4bf feat: add modal login gacha page 2025-03-29 17:26:54 +07:00
Hafid Nur 1594be9d46 feat: slicing modal backoffice
- Slicing desain modal untuk backoffice di storybook component ModalBackoffice
- Update static directory untuk storybook agar dapat membaca gambar pada folder public
- Minor edit component button & input
2025-03-29 02:34:02 +07:00
Hafid Nur 7eecf2621d feat: add filter popover component for delivery status
- Tambah fungsional close button untuk komponen Filter
- Penyesuaian warna neutral sesuai Design System
2025-03-29 01:17:32 +07:00
Hafid Nur 088a0fc41f feat: Add Filter component 2025-03-29 00:59:25 +07:00
Hafid Nur 9178c65c4a feat: Add page "Data Pengiriman Hadiah"
- Update sidebar link untuk halaman Data Pengiriman Hadiah
- Penyesuaian kecil pada style DataTable dan header page
2025-03-28 22:41:55 +07:00
Hafid Nur f93d741cef feat: Minor adjustment
- Optimize gambar dengan webp
- Perlebar tampilan utama di layar besar
- Fix DataTable props
2025-03-28 21:46:10 +07:00
Naufal AzmiandGitHub 81fa73935f Merge branch 'develop' into DimentorinAuth 2025-03-28 19:09:37 +07:00
Maulana SodiqinandGitHub 88c71ccdfa Merge pull request #6 from IMPHNEN/backoffice/auth
Backoffice Login, Dashboard, Accounts, Transactions page
2025-03-28 20:05:34 +08:00
Hafid Nur 5b3b9e44fa feat: Implement DataTable to backoffice pages
- Halaman Data Akun
- Halaman Validasi Transaksi
2025-03-28 18:50:54 +07:00
Hafid Nur ac2ff2ec92 feat: Combine Pagination component into DataTable 2025-03-28 18:50:17 +07:00
Hafid Nur 79e0d39280 feat: separate DataTable component 2025-03-28 18:17:13 +07:00
Hafid Nur fd9113109c feat: Working TanStack pagination
- Menerapkan TanStack Table dan Pagination
2025-03-28 17:29:06 +07:00
Hafid Nur da7e1067d2 feat: Implement pagination
- Terapkan pagination pada page Data Akun
2025-03-28 16:50:58 +07:00
Hafid Nur 75779b0373 feat: Simple implementation TanStack table
Coba implementasikan versi paling sederhana TanStack Table v8 langsung ke page Data Akun
2025-03-28 16:21:27 +07:00
boboiazumi a3887519f9 feat: Login Page 2025-03-28 11:46:27 +07:00
Hafid Nur 2ec6ad5b5c feat: Add TanStack Table v8 and implement it to Datatable component 2025-03-28 00:19:01 +07:00
Hafid Nur 2d541c71ba Merge branch into backoffice/auth 2025-03-27 23:19:45 +07:00
Hafid Nur 46f3bd6014 fix: Sidebar stick to left. Fit content for different desktop viewports 2025-03-27 23:17:43 +07:00
Hafid Nur c374c97e66 fix: Sidebar stick to left. Fit content for different desktop viewports 2025-03-27 23:15:11 +07:00
Hafid Nur a4ca53004f feat: fix datatable storybook 2025-03-27 22:52:35 +07:00
Hafid Nur 2d9473ea7c feat: Adjustment
- Sesuaikan color palette success, info, danger, warning sesuai dengan design system Figma
- Sesuai background pagination
- Fix width form login tidak maksimal
2025-03-27 22:28:40 +07:00
Hafid Nur 6bfa48bed8 feat: Change data table component dynamically
- Ubah komponen DataTable agar bisa render menyesuaikan jumlah kolom dan baris pada data
2025-03-27 17:22:04 +07:00
Hafid Nur 43167fe7a1 feat: Draft transaction page 2025-03-27 16:58:24 +07:00
Hafid Nur 39391e5522 feat: Page Data Akun 2025-03-27 16:55:54 +07:00
Hafid Nur ec509a7b43 feat: Styling pagination 2025-03-27 16:41:42 +07:00
Hafid Nur a76bd89ec1 feat: Styling data table component 2025-03-27 16:29:09 +07:00
Hafid Nur cafdb29024 feat: Refactor datatable and pagination component and add storybook
- Refactor nama component dari datatable dan pagination mengikuti codebase
- Buat storybook untuk preview dari masing-masing komponen
2025-03-27 15:59:02 +07:00
Hafid Nur d79d0d3d09 feat: Draft Data Akun page
- Vibes coding untuk siapkan base dari halaman Data Akun
2025-03-27 15:17:04 +07:00
Hafid Nur 7606c5b4e2 feat: Divider on top of logout button sidebar 2025-03-27 15:08:22 +07:00
Hafid Nur bc9a0fd740 feat: Another page template
- Template untuk page Data Akun dan Validasi Transaksi
2025-03-27 15:06:30 +07:00
Hafid Nur 7f9a106fca feat: Fix gacha image preview
- Menggunakan pendekatan simpan gambar clipped langsung daripada mengatur secara dinamis menggunakan CSS object-position
2025-03-27 15:00:04 +07:00
Hafid Nur 0bcc8f7f6a feat: gacha section in dashboard 2025-03-27 14:31:30 +07:00
Hafid Nur 31b7587571 feat: summary section in dashboard page 2025-03-27 13:35:13 +07:00
Hafid Nur 82fb1ea2bc feat: Update sidebar
- Perbaiki tampilan sidebar sesuai desain
2025-03-27 13:18:14 +07:00
Hafid Nur bd5e46e712 feat: Draft dashboard AI-generated 2025-03-27 07:46:15 +07:00
Hafid Nur 542469bc67 feat: Update InputForm, Input, and auth page
- Perbaiki page login ke halaman utama backoffice
- Perbaiki component Input dan InputForm
2025-03-27 07:19:46 +07:00
Hafid Nur 631684cb05 feat: Update InputForm to handle different sizes
- Tambah spec test pada komponen InputForm
- Tambah autodocs pada komponen Input
2025-03-27 06:00:03 +07:00
Hafid Nur 54f4572533 feat: Update InputForm component
- Perbarui docs untuk component Input dan InputForm
- Perbarui component InputForm agar bekerja dengan helper text, error, dan label/input
2025-03-27 05:25:02 +07:00
Hafid Nur ca933f624a feat: Create a separate "InputForm" molecule component
- Develop the "InputForm" molecule component
- Remove empty modal-gacha component stories and test files that prevent Storybook from running
2025-03-27 04:35:41 +07:00
Hafid Nur 90ab974e03 feat: backoffice login page 2025-03-27 02:42:57 +07:00
Maulana Sodiqin daeeb09798 feat: integrate with new api 2025-03-19 23:38:42 +07:00
Maulana SodiqinandGitHub c8935b885e Merge pull request #5 from IMPHNEN/ModalsGacha
Modals gacha
2025-03-17 18:34:21 +07:00
boboiazumi 4ac1278a33 fix: conflict 2025-03-17 18:13:24 +07:00
Maulana Sodiqin d20f0d89e5 feat: init backoffice 2025-03-17 12:48:11 +07:00
Maulana Sodiqin 515b78c57f feat: add backoffice apps 2025-03-17 09:39:04 +07:00
Maulana SodiqinandGitHub 8a53575cd1 Merge pull request #4 from IMPHNEN/feat/simple-landing-page
Roulette page
2025-03-17 09:29:43 +07:00
Hafid Nur c8b12db1b3 feat: linear gradient diffuser 2025-03-16 22:53:13 +07:00
Hafid Nur e3c4b7a23a feat: infinite scroll animation for gacha items 2025-03-16 22:25:29 +07:00
boboiazumi 41a0842026 feat: modals-gacha 2025-03-16 22:18:48 +07:00
Hafid Nur a9164e5e5f feat: gacha item horizontal scroll snap 2025-03-16 22:16:52 +07:00
Hafid Nur d7e39095bb feat: separate GachaItem component and add more Gacha items (images and labels)
TODO:
- implement an infinite scroll snap slider for Gacha items
2025-03-16 21:42:45 +07:00
Hafid Nur 84ddf2b771 feat: draft roulette page 2025-03-16 20:40:18 +07:00
Hafid Nur cb632adbb4 feat: change the layout of the landing page from using CSS Flexbox to CSS Grid
- minor adjustment for navbar max width and padding
2025-03-16 19:54:42 +07:00
Hafid Nur 6a1fa42956 feat: update navbar to cover top of the page
- Update the navbar to be sticky when scrolling the page and cover the top of the page.
- Fix the margins for every viewport.
2025-03-16 19:19:44 +07:00
Hafid Nur 9002e6a261 feat: merge landing page component into single page file
- Component landing page sebelumnya terpisah, tetapi perlu digabungkan lagi karena tidak reusable pada aplikasi
2025-03-16 19:13:49 +07:00
Maulana SodiqinandGitHub 254d4fe04a Merge pull request #2 from IMPHNEN/feat/simple-landing-page
Slicing simple landing page
2025-03-16 12:39:12 +07:00
Hafid Nur 02e06caca1 feat: landing page for desktop viewport 2025-03-16 07:41:16 +07:00
Hafid Nur 00033b9c68 feat: landing page for tablet viewport
- change "scroll for gacha" to button to roulette page
2025-03-16 07:11:06 +07:00
Hafid Nur 02863ebc8b feat: add landing page for mobile viewport 2025-03-16 06:38:14 +07:00
Hafid Nur a31166e3ff feat: update navbar responsive 2025-03-16 06:07:07 +07:00
Hafid Nur c7b74bc9b6 feat: update theme to match the Design System
adjustment
- primary color
- base + typography size
2025-03-16 05:42:24 +07:00
Maulana SodiqinandGitHub 01621a8786 Merge pull request #1 from IMPHNEN/feat/component-migrate
Migrate component from 'imphnen-gacha' repository
2025-03-16 01:45:33 +07:00
hafidnrzs a07e3c21d6 feat: add Textarea component 2025-03-16 01:40:02 +07:00
hafidnrzs fb99b41b3a feat: add PasswordInput component 2025-03-16 01:34:24 +07:00
Hafid Nur 183ca100dc feat: add Input component 2025-03-16 00:56:02 +07:00
Maulana Sodiqin 203ee5de97 chore: remove uneeded folder/file 2025-03-16 00:35:48 +07:00
Maulana Sodiqin 58843041db faet 2025-03-16 00:33:48 +07:00
Maulana Sodiqin 3964c22c66 feat: navbar unit test 2025-03-16 00:16:58 +07:00
Maulana Sodiqin ec02a4ef83 fix: unit test 2025-03-16 00:12:15 +07:00
Maulana Sodiqin 587cb9facf fix: ui error build 2025-03-16 00:07:02 +07:00
Maulana Sodiqin 5c5faaf8a1 fix: libs cannot apply style 2025-03-16 00:05:17 +07:00
181 changed files with 14041 additions and 731 deletions
+1
View File
@@ -0,0 +1 @@
VITE_API_URL=https://api.example.com
+30
View File
@@ -0,0 +1,30 @@
## Issue
<!-- #GitHubIssueNumber / Issue Link / Describe the problem here (if the issue hasn't been defined) -->
## What did you do?
<!--
- Describe what did you do in this code
- Example:
- [CORRECT] Add auth feature / Modify auth feature.
- [WRONG] Create file /controllers/auth.ts, /models/user.ts, modify /routes/web.ts
-->
## Screenshot / Video
<!--
Attach pictures of this work / video explanation about this work
-->
## Note for Deployment
<!--
- Tell to DevOps what should he do to deploy this change
- example:
- Please run migration when deploy this
- Please upload these files into folder xxx
- Please create folder `xxx` and give permission `777`
- Please run this SQL after deploy ```sql INSERT INTO xxx (...) ```
- etc...
-->
+8
View File
@@ -43,3 +43,11 @@ Thumbs.db
vite.config.*.timestamp* vite.config.*.timestamp*
vitest.config.*.timestamp* vitest.config.*.timestamp*
storybook-static
.env
.env.prod
.env.develop
.env.staging
+87 -71
View File
@@ -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&amp;utm_medium=readme&amp;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 ## How to run
npx nx serve dimentorin
```
To create a production bundle: ### Development
```sh Use the following commands to run in development mode:
npx nx build dimentorin
```
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 ### Build
npx nx show project dimentorin
```
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 &raquo;](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 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:
npx nx g @nx/react:app demo
```
To generate a new library, use: - **Run Storybook**
```sh This command starts Storybook in development mode, allowing you to view and test UI components interactively.
npx nx g @nx/react:lib mylib
```
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. ```sh
npm run ui:storybook
```
[Learn more about Nx plugins &raquo;](https://nx.dev/concepts/nx-plugins?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) | [Browse the plugin registry &raquo;](https://nx.dev/plugin-registry?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
## Set up CI! - **Run Unit Test**
### Step 1 This command runs unit tests for the UI components to ensure they function as expected.
To connect to Nx Cloud, run the following command: ```sh
npm run ui:test
```
```sh - **Build Components**
npx nx connect
```
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: This command generates a static build of Storybook, which can be deployed for sharing and documentation purposes.
- [Remote caching](https://nx.dev/ci/features/remote-cache?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) ```sh
- [Task distribution across multiple machines](https://nx.dev/ci/features/distribute-task-execution?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) npm run ui:build
- [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)
### Step 2 ## How to contribute
Use the following command to configure a CI workflow for your workspace: 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.
```sh If you encounter any issues or problems, feel free to create a new Issue.
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 &raquo;](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&amp;utm_medium=readme&amp;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)
+12
View File
@@ -0,0 +1,12 @@
import nx from '@nx/eslint-plugin';
import baseConfig from '../../eslint.config.mjs';
export default [
...baseConfig,
...nx.configs['flat/react'],
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
},
];
+16
View File
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Backoffice</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/logos/simple.svg" />
<link rel="stylesheet" href="/src/styles.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
import { join } from 'path';
export default {
plugins: {
'@tailwindcss/postcss': {
base: join(import.meta.dirname, '../../'),
},
},
};
+9
View File
@@ -0,0 +1,9 @@
{
"name": "backoffice",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/backoffice/src",
"projectType": "application",
"tags": [],
"// targets": "to see all targets run: nx show project backoffice --web",
"targets": {}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 351 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 511 KiB

@@ -0,0 +1,31 @@
import { useForm } from 'react-hook-form';
import {
authLoginSchema,
TLoginRequest,
usePostLogin,
} from '@imphnen-frontend-service/service';
import { zodResolver } from '@hookform/resolvers/zod';
import { useNavigate } from 'react-router-dom';
export const useLogin = () => {
const postLogin = usePostLogin();
const form = useForm<TLoginRequest>({
resolver: zodResolver(authLoginSchema),
mode: 'all',
});
const navigate = useNavigate();
const onSubmit = form.handleSubmit((data) => {
postLogin.mutate(data, {
onSuccess: () => {
console.log('Success Login');
navigate('/dashboard');
},
});
});
return {
form,
onSubmit,
};
};
@@ -0,0 +1,162 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
import { useState } from 'react';
interface IModalEditAccount {
isOpen: boolean;
onClose: () => void;
handleEditAccount?: () => void;
currentStep?: number;
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
}
const ModalEditAccount = ({
isOpen,
onClose,
currentStep,
nextStep,
prevStep,
resetStep,
handleEditAccount,
}: IModalEditAccount) => {
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}
handleEditAccount={handleEditAccount}
resetStep={resetStep}
/>
)}
</Modal>
);
};
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
}
const StepOne = ({ nextStep }: IStepOneProps) => {
const [fullName, setFullName] = useState('Ahmad Wiyana');
const [email, setEmail] = useState('fullname23@gmail.com');
const [phoneNumber, setPhoneNumber] = useState('081904423804');
const [address, setAddress] = useState('Jl. Pantai Cibaduyut Indah');
return (
<>
<Modal.Header>
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Edit Data Akun
</h2>
</Modal.Header>
<Modal.Content className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<InputField
label="Nama Lengkap"
type="text"
placeholder="Masukkan Nama Lengkap"
value={fullName}
onChange={(e) => setFullName(e.target.value)}
size="lg"
className="w-full"
/>
<InputField
label="Email"
type="text"
placeholder="Masukkan Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
size="lg"
className="w-full"
/>
<InputField
label="Nomor Telepon"
type="text"
placeholder="Masukkan Nomor Telepon"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
size="lg"
className="w-full"
/>
<InputField
label="Alamat"
type="text"
placeholder="Masukkan Alamat"
value={address}
onChange={(e) => setAddress(e.target.value)}
size="lg"
className="w-full"
/>
</div>
<Button
variant="primary"
size="lg"
className="w-full"
onClick={nextStep}
>
Perbarui Data
</Button>
</Modal.Content>
</>
);
};
interface IStepTwoProps {
onClose: () => void;
handleEditAccount?: () => void;
resetStep: () => void;
}
const StepTwo = ({ onClose, handleEditAccount, resetStep }: IStepTwoProps) => (
<>
<Modal.Header className="mb-0 text-center items-center">
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Update Data
</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={() => {
handleEditAccount && handleEditAccount();
onClose();
resetStep();
}}
>
Update
</Button>
</Modal.Content>
</>
);
export default ModalEditAccount;
@@ -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;
+195
View File
@@ -0,0 +1,195 @@
import * as React from 'react';
import { FC, Fragment, ReactElement, useState } from 'react';
import {
FilterOutlined,
SearchOutlined,
EditOutlined,
} from '@ant-design/icons';
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms';
import {
ColumnDef,
getCoreRowModel,
getPaginationRowModel,
PaginationState,
useReactTable,
RowSelectionState,
} from '@tanstack/react-table';
import ModalEditAccount from './_components/modal-edit-account';
import { useQueryState } from '../../hook/use-query-state';
interface Account {
id: number;
name: string;
email: string;
phone: string;
address: string;
}
// Mock data for demonstration
const mockData: Account[] = Array.from({ length: 90 }, (_, i) => ({
id: i + 1,
name: i === 0 ? 'Ahmad Wijuana' : 'Nama Lengkap',
email: 'fullname23@gmail.com',
phone: '081904423804',
address: 'Jl. Pantai Cibaduyut Indah',
}));
export const Components: FC = (): ReactElement => {
const [showModalEditAccount, setShowModalEditAccount] = 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 [showFilter, setShowFilter] = useState(false);
const columns: ColumnDef<Account>[] = [
{
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 Lengkap',
accessorKey: 'name',
},
{
header: 'Email',
accessorKey: 'email',
},
{
header: 'Nomor Telp',
accessorKey: 'phone',
},
{
header: 'Alamat Pengiriman',
accessorKey: 'address',
},
{
header: 'Action',
cell: ({ row }) => (
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation();
setShowModalEditAccount(true);
}}
className="flex items-center gap-2"
>
<EditOutlined /> Edit
</Button>
),
},
];
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 */}
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
<h1 className="text-p2 font-semibold">Data Akun</h1>
</header>
{/* Account Table Section */}
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
{/* Search and Filter */}
<div className="flex justify-between items-center gap-8 mb-2">
<div className="relative w-full">
<Input
placeholder="Cari berdasarkan nama lengkap, email"
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 items-center gap-3"
disabled
onClick={() => setShowFilter(!showFilter)}
>
<FilterOutlined />
Filters
</Button>
{showFilter && (
<div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg">
<Filter onClose={() => setShowFilter(false)} />
</div>
)}
</div>
</div>
{/* Table */}
<DataTable data={mockData} columns={columns} table={table} />
</section>
</main>
{/* Modal Edit Account */}
<ModalEditAccount
currentStep={currentStep}
isOpen={showModalEditAccount}
onClose={() => setShowModalEditAccount(false)}
handleEditAccount={() => {
console.log('Account updated');
}}
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
/>
</Fragment>
);
};
export default Components;
@@ -0,0 +1,136 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
interface IModalAddItem {
isOpen: boolean;
onClose: () => void;
handleAddItem: () => void;
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) => (
<>
<Modal.Header>
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Tambah Item Gacha
</h2>
<p className="text-p3 text-neutral-400">
Lengkapi detail di bawah ini untuk menambahkan item gacha
</p>
</Modal.Header>
<Modal.Content className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<InputField
label="Nama Hadiah"
type="text"
placeholder="Masukkan Nama Hadiah"
size="lg"
className="w-full"
/>
<InputField
label="Chance Rate"
type="text"
placeholder="Masukkan Chance Rate"
size="lg"
className="w-full"
/>
<InputField
label="Foto Barang"
type="file"
placeholder=".jpg, .jpeg, atau .png"
size="lg"
className="w-full"
/>
</div>
<Button variant="primary" size="lg" className="w-full" onClick={nextStep}>
Tambahkan Item
</Button>
</Modal.Content>
</>
);
interface IStepTwoProps {
onClose: () => void;
handleAddItem?: () => void;
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>
</>
);
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,136 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
interface IModalEditItem {
isOpen: boolean;
onClose: () => void;
handleEditItem?: () => void;
currentStep?: number;
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
}
const ModalEditItem = ({
isOpen,
onClose,
currentStep,
nextStep,
resetStep,
handleEditItem,
}: IModalEditItem) => {
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}
handleEditItem={handleEditItem}
resetStep={resetStep}
/>
)}
</Modal>
);
};
interface IStepOneProps {
nextStep: () => void;
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>
<Button variant="primary" size="lg" className="w-full" onClick={nextStep}>
Perbarui Item
</Button>
</Modal.Content>
</>
);
interface IStepTwoProps {
onClose: () => void;
handleEditItem?: () => void;
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>
</>
);
export default ModalEditItem;
@@ -0,0 +1,18 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
export const AppLayout: FC = (): ReactElement => {
return (
<div className="bg-primary-50 min-h-screen flex justify-center">
<div className="bg-primary-50 min-h-screen w-full flex">
<BackofficeSidebar />
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
<Outlet />
</div>
</div>
</div>
);
};
export default AppLayout;
+210
View File
@@ -0,0 +1,210 @@
import {
PlusOutlined,
ReloadOutlined,
UsergroupAddOutlined,
UsergroupDeleteOutlined,
UserSwitchOutlined,
} from '@ant-design/icons';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { FC, Fragment, ReactElement, useState } from 'react';
import ModalAddItem from './_components/modal-add-item';
import ModalEditItem from './_components/modal-edit-item';
import ModalDeleteItem from './_components/modal-delete-item';
import { useQueryState } from '../../hook/use-query-state';
export const Components: FC = (): ReactElement => {
const [showModalAddItem, setShowModalAddItem] = useState(false);
const [showModalEditItem, setShowModalEditItem] = useState(false);
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
const {
step: currentStep,
nextStep,
prevStep,
resetStep,
} = useQueryState('step', {
defaultValue: 1,
maxValue: 2,
minValue: 1,
});
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]" />
</div>
<div className="flex flex-col gap-1">
<h3 className="text-p1 font-semibold">1000</h3>
<p className="text-label1 text-neutral-500">Participants</p>
</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]" />
</div>
<div className="flex flex-col gap-1">
<h3 className="text-p1 font-semibold">1000</h3>
<p className="text-label1 text-neutral-500">
Roll and Reroll
</p>
</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]" />
</div>
<div className="flex flex-col gap-1">
<h3 className="text-p1 font-semibold">1000</h3>
<p className="text-label1 text-neutral-500">Redeem</p>
</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]" />
</div>
<div className="flex flex-col gap-1">
<h3 className="text-p1 font-semibold">1000</h3>
<p className="text-label1 text-neutral-500">
Inactive Users
</p>
</div>
</div>
</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">
Gacha Items
</h2>
<Button
variant="primary"
size="sm"
className="items-end gap-3"
onClick={() => setShowModalAddItem(true)}
>
<span>Tambah Item</span>
<PlusOutlined className="text-[16px]" />
</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"
>
<div className="flex flex-col py-4 px-6 gap-4">
<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">
<span>Prize {item}</span>
<span>Chance Rate: (0.1%)</span>
</div>
</div>
<div className="flex justify-start gap-2">
<Button
variant="text"
size="sm"
className="text-[10px] text-neutral-500 p-0 font-normal hover:bg-transparent hover:text-primary-500"
onClick={() => setShowModalEditItem(true)}
>
Edit
</Button>
<Button
variant="text"
size="sm"
className="text-[10px] text-red-500 p-0 font-normal hover:bg-transparent hover:text-red-700"
onClick={() => setShowModalDeleteItem(true)}
>
Delete
</Button>
</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"
/>
</div>
))}
</div>
</section>
</div>
{/* Right-side illustration */}
<img
src="gacha.webp"
alt=""
className="rounded-lg hidden xl:block xl:min-w-[436px] h-auto object-cover"
/>
</div>
</main>
{/* Modal Add Item */}
<ModalAddItem
currentStep={currentStep}
isOpen={showModalAddItem}
onClose={() => setShowModalAddItem(false)}
handleAddItem={() => {
console.log('Item added');
}}
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
/>
{/* Modal Edit Item */}
<ModalEditItem
currentStep={currentStep}
isOpen={showModalEditItem}
onClose={() => setShowModalEditItem(false)}
handleEditItem={() => {
console.log('Item edited');
}}
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
/>
{/* Modal Delete Item */}
<ModalDeleteItem
isOpen={showModalDeleteItem}
onClose={() => setShowModalDeleteItem(false)}
handleDeleteItem={() => {
console.log('Item deleted');
}}
/>
</Fragment>
);
};
export default Components;
+12
View File
@@ -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;
+44
View File
@@ -0,0 +1,44 @@
import { FC, ReactElement } from 'react';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { useLogin } from './_hooks/use-login';
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
export const Components: FC = (): ReactElement => {
const { form, onSubmit } = useLogin();
return (
<div className="flex justify-center items-center min-h-screen">
<div className="bg-white border border-primary-200 shadow-lg p-[60px] text-center flex flex-col justify-items-stretch gap-8 rounded-2xl">
<img src="/logos/logo.svg" alt="" className="h-[70px] w-auto" />
<h1 className="text-primary-500 text-p1 font-semibold">
Welcome to IMPHNEN Backoffice
</h1>
<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>
);
};
export default Components;
@@ -0,0 +1,84 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
interface IModalProcessDelivery {
isOpen: boolean;
onClose: () => void;
handleProcessDelivery?: () => void;
}
const ModalProcessDelivery = ({
isOpen,
onClose,
handleProcessDelivery,
}: IModalProcessDelivery) => {
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
isOpen={isOpen}
onClose={onClose}
disableEscapeKeyDown={true}
>
<Modal.Header>
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Delivery Process
</h2>
<p className="text-p3 text-neutral-400">
Lakukan pengiriman hadiah gacha untuk pengguna di bawah ini, jika
sudah ubah status menjadi Delivered.
</p>
</Modal.Header>
<Modal.Content className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<InputField
label="Nama Lengkap"
type="text"
placeholder="Masukkan Nama Lengkap"
value="Ahmad Wiyana"
size="lg"
className="w-full"
readOnly
/>
<InputField
label="Item yang didapatkan"
type="text"
placeholder="Masukkan Nama Item"
value="Lanyard + ID Card"
size="lg"
className="w-full"
readOnly
/>
<InputField
label="Alamat Pengiriman"
type="text"
placeholder="Masukkan Alamat Pengiriman"
value="Jl. Pantai Cibaduyut Indah"
size="lg"
className="w-full"
readOnly
/>
<InputField
label="Status"
type="text"
placeholder="Isi Status Pengiriman"
value="Lanyard + ID Card"
size="lg"
className="w-full"
readOnly
/>
</div>
<Button
variant="primary"
size="lg"
className="w-full"
onClick={() => handleProcessDelivery && handleProcessDelivery()}
>
Proses Pengiriman
</Button>
</Modal.Content>
</Modal>
);
};
export default ModalProcessDelivery;
+18
View File
@@ -0,0 +1,18 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
export const AppLayout: FC = (): ReactElement => {
return (
<div className="bg-primary-50 min-h-screen flex justify-center">
<div className="bg-primary-50 min-h-screen w-full flex">
<BackofficeSidebar />
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
<Outlet />
</div>
</div>
</div>
);
};
export default AppLayout;
+251
View File
@@ -0,0 +1,251 @@
import * as React from 'react';
import { FC, Fragment, ReactElement, useState } from 'react';
import {
FilterOutlined,
SearchOutlined,
AuditOutlined,
} from '@ant-design/icons';
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms';
import {
ColumnDef,
getCoreRowModel,
getPaginationRowModel,
PaginationState,
useReactTable,
RowSelectionState,
} from '@tanstack/react-table';
import ModalProcessDelivery from './_components/modal-process-item';
type OrderValid = 'valid' | 'invalid' | 'unchecked';
type Status = 'undelivered' | 'delivered';
interface Prize {
id: number;
name: string;
orderValid: OrderValid;
items: string;
address: string;
status: Status;
}
const items = [
'Sertifikat + Laminating',
'Lanyard + ID Card',
'Pin',
'Sticker Isi 3',
'Sticker Isi 5',
'Gelang Karet',
];
// Mock data for transactions
const mockData: Prize[] = Array.from({ length: 90 }, (_, i) => ({
id: i + 1,
name: 'Nama Lengkap',
orderValid: (i % 3 === 0
? 'invalid'
: i % 5 === 0
? 'unchecked'
: 'valid') as OrderValid,
items: items[i % items.length],
address: 'Jl. Pantai Cibaduyut Indah',
status: (i % 3 === 0 ? 'undelivered' : 'delivered') as Status,
}));
export const Components: FC = (): ReactElement => {
const [showModalProcessDelivery, setShowModalProcessDelivery] =
useState(false);
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 9,
});
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
const [showFilter, setShowFilter] = useState(false);
const deliveryOptions = [
{ id: 'option1', value: 'undelivered', label: 'Undelivered' },
{ id: 'option1', value: 'delivered', label: 'Delivered' },
];
const columns: ColumnDef<Prize>[] = [
{
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 Lengkap',
accessorKey: 'name',
},
{
header: 'Order Valid?',
accessorKey: 'orderValid',
cell: ({ row }) => {
const status = row.original.orderValid;
const statusColors: Record<OrderValid, string> = {
valid: 'bg-success-200 text-success-500',
invalid: 'bg-danger-200 text-danger-500',
unchecked: 'bg-warning-200 text-warning-900',
};
const statusText: Record<OrderValid, string> = {
valid: 'Valid',
invalid: 'Invalid',
unchecked: 'Unchecked',
};
return (
<div
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
>
{statusText[status]}
</div>
);
},
},
{
header: 'Items',
accessorKey: 'items',
},
{
header: 'Alamat Pengiriman',
accessorKey: 'address',
},
{
header: 'Status',
accessorKey: 'status',
cell: ({ row }) => {
const status = row.original.status;
const statusColors: Record<Status, string> = {
delivered: 'bg-success-200 text-success-500',
undelivered: 'bg-danger-200 text-danger-500',
};
const statusText: Record<Status, string> = {
delivered: 'Delivered',
undelivered: 'Undelivered',
};
return (
<div
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
>
{statusText[status]}
</div>
);
},
},
{
header: 'Action',
cell: ({ row }) => (
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation();
setShowModalProcessDelivery(true);
}}
className="flex items-center gap-2 w-full"
>
<AuditOutlined className="text-[16px]" /> Process
</Button>
),
},
];
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 */}
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
<h1 className="text-p2 font-semibold">Data Pengiriman Hadiah</h1>
</header>
{/* Account Table Section */}
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
{/* Search and Filter */}
<div className="flex justify-between items-center gap-8 mb-2">
<div className="relative w-full">
<Input
placeholder="Cari berdasarkan nama lengkap, nomor order Shopee"
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 items-center gap-3"
onClick={() => setShowFilter(!showFilter)}
>
<FilterOutlined />
Filters
</Button>
{showFilter && (
<div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg">
<Filter
options={deliveryOptions}
onClose={() => setShowFilter(false)}
onFilterChange={(value) => {
console.log('Selected filter:', value);
// Filter logic di sini
}}
/>
</div>
)}
</div>
</div>
{/* Table */}
<DataTable data={mockData} columns={columns} table={table} />
</section>
</main>
{/* Modal Process Delivery */}
<ModalProcessDelivery
isOpen={showModalProcessDelivery}
onClose={() => setShowModalProcessDelivery(false)}
handleProcessDelivery={() => {
console.log('Action ketika user menekan tombol Proses Pengiriman');
}}
/>
</Fragment>
);
};
export default Components;
@@ -0,0 +1,61 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
interface IModalValidate {
isOpen: boolean;
onClose: () => void;
handleValid?: () => void;
handleInvalid?: () => void;
}
const ModalValidate = ({
isOpen,
onClose,
handleValid,
handleInvalid,
}: IModalValidate) => {
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
isOpen={isOpen}
onClose={onClose}
disableEscapeKeyDown={true}
>
<Modal.Header className="mb-0 text-center items-center">
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Validasi Transaksi
</h2>
</Modal.Header>
<Modal.Content className="flex flex-col gap-8">
<InputField
label="Nomor Transaksi"
type="text"
placeholder="Masukkan Nomor Transaksi"
value="2502133Y9AFVBO"
size="lg"
className="w-full"
/>
<div className="flex gap-4">
<Button
variant="bordered"
size="lg"
className="w-full border-danger-500 text-danger-500 hover:border-danger-700 hover:text-danger-700"
onClick={() => handleInvalid && handleInvalid()}
>
Tidak Valid
</Button>
<Button
variant="primary"
size="lg"
className="w-full"
onClick={() => handleValid && handleValid()}
>
Valid
</Button>
</div>
</Modal.Content>
</Modal>
);
};
export default ModalValidate;
@@ -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,214 @@
import * as React from 'react';
import { FC, Fragment, ReactElement, useState } from 'react';
import {
FilterOutlined,
SearchOutlined,
AuditOutlined,
} from '@ant-design/icons';
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms';
import {
ColumnDef,
getCoreRowModel,
getPaginationRowModel,
PaginationState,
useReactTable,
RowSelectionState,
} from '@tanstack/react-table';
import ModalValidate from './_components/modal-validate';
type TransactionStatus = 'valid' | 'invalid' | 'unchecked';
interface Transaction {
id: number;
name: string;
transactionNumber: string;
status: TransactionStatus;
}
// Mock data for transactions
const mockTransactions: Transaction[] = Array.from({ length: 20 }, (_, i) => ({
id: i + 1,
name: i === 0 ? 'Ahmad Wijuana' : 'Nama Lengkap',
transactionNumber: '25D2133Y9AFYBD',
status: (i % 3 === 0
? 'invalid'
: i % 5 === 0
? 'unchecked'
: 'valid') as TransactionStatus,
}));
export const Components: FC = (): ReactElement => {
const [showModalValidate, setShowModalValidate] = useState(false);
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 9,
});
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
const [showFilter, setShowFilter] = useState(false);
const validationOptions = [
{ id: 'option1', value: 'unchecked', label: 'Unchecked' },
{ id: 'option2', value: 'valid', label: 'Valid' },
{ id: 'option3', value: 'invalid', label: 'Invalid' },
];
const columns: ColumnDef<Transaction>[] = [
{
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 Lengkap',
accessorKey: 'name',
},
{
header: 'Nomor Transaksi',
accessorKey: 'transactionNumber',
},
{
header: 'Order Valid?',
accessorKey: 'status',
cell: ({ row }) => {
const status = row.original.status;
const statusColors: Record<TransactionStatus, string> = {
valid: 'bg-success-200 text-success-500',
invalid: 'bg-danger-200 text-danger-500',
unchecked: 'bg-warning-200 text-warning-900',
};
const statusText: Record<TransactionStatus, string> = {
valid: 'Valid',
invalid: 'Invalid',
unchecked: 'Unchecked',
};
return (
<div
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
>
{statusText[status]}
</div>
);
},
},
{
header: 'Action',
cell: ({ row }) => (
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation();
setShowModalValidate(true);
}}
className="flex items-center gap-2 w-full"
>
<AuditOutlined className="text-[16px]" /> Update
</Button>
),
},
];
const table = useReactTable({
data: mockTransactions,
columns,
state: {
pagination,
rowSelection,
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(mockTransactions.length / pagination.pageSize),
manualPagination: false,
});
return (
<Fragment>
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
{/* Header */}
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
<h1 className="text-p2 font-semibold">Validasi Transaksi</h1>
</header>
{/* Account Table Section */}
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
{/* Search and Filter */}
<div className="flex justify-between items-center gap-8 mb-2">
<div className="relative w-full">
<Input
placeholder="Cari berdasarkan nama lengkap, nomor order Shopee"
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 items-center gap-3"
onClick={() => setShowFilter(!showFilter)}
>
<FilterOutlined />
Filters
</Button>
{showFilter && (
<div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg">
<Filter
options={validationOptions}
onClose={() => setShowFilter(false)}
onFilterChange={(value) => {
console.log('Selected filter:', value);
// Filter logic di sini
}}
/>
</div>
)}
</div>
</div>
{/* Table */}
<DataTable data={mockTransactions} columns={columns} table={table} />
</section>
</main>
<ModalValidate
isOpen={showModalValidate}
onClose={() => setShowModalValidate(false)}
handleValid={() => {
console.log('Action ketika user klik Valid');
}}
handleInvalid={() => {
console.log('Action ketika user klik Tidak Valid');
}}
/>
</Fragment>
);
};
export default Components;
@@ -0,0 +1,71 @@
import { useCallback, useEffect, useState } from 'react';
interface UseQueryStateOptions {
defaultValue: number;
maxValue?: number;
minValue?: number;
}
export const useQueryState = (key: string, options: UseQueryStateOptions) => {
const { defaultValue, maxValue = Infinity, minValue = 1 } = options;
const getQueryParam = (param: string): string | null => {
if (typeof window === 'undefined') return null;
const searchParams = new URLSearchParams(window.location.search);
return searchParams.get(param);
};
const setQueryParam = (param: string, value: string) => {
const searchParams = new URLSearchParams(window.location.search);
searchParams.set(param, value);
const newUrl = `${window.location.pathname}?${searchParams.toString()}`;
window.history.replaceState(null, '', newUrl);
};
const initialValue = () => {
const queryValue = getQueryParam(key);
const parsedValue = queryValue ? parseInt(queryValue, 10) : defaultValue;
return Math.max(minValue, Math.min(maxValue, parsedValue));
};
const [value, setValue] = useState<number>(initialValue);
useEffect(() => {
const handlePopState = () => {
const queryValue = getQueryParam(key);
const newValue = queryValue ? parseInt(queryValue, 10) : defaultValue;
setValue(Math.max(minValue, Math.min(maxValue, newValue)));
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, [key, defaultValue, minValue, maxValue]);
const updateValue = useCallback(
(newValue: number) => {
const constrainedValue = Math.max(minValue, Math.min(maxValue, newValue));
setValue(constrainedValue);
setQueryParam(key, constrainedValue.toString());
},
[key, minValue, maxValue]
);
const nextStep = useCallback(() => {
updateValue(value + 1);
}, [value, updateValue]);
const prevStep = useCallback(() => {
updateValue(value - 1);
}, [value, updateValue]);
const resetStep = useCallback(() => {
updateValue(defaultValue);
}, [defaultValue, updateValue]);
return {
step: value,
nextStep,
prevStep,
resetStep,
};
};
+136
View File
@@ -0,0 +1,136 @@
@import url('https://fonts.googleapis.com/css2?family=Bai+Jamjuree:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&display=swap');
@import 'tailwindcss';
@source "../../../libs/ui/**/*.{ts,tsx}";
@theme {
--color-primary-50: #f0f8ff;
--color-primary-100: #e1f0fd;
--color-primary-200: #bce1fb;
--color-primary-300: #81cbf8;
--color-primary-400: #3eb0f2;
--color-primary-500: #23a1eb;
--color-primary-600: #0877c1;
--color-primary-700: #085f9c;
--color-primary-800: #0b5181;
--color-primary-900: #0f446b;
--color-primary-950: #0a2b47;
--color-neutral-50: #f6f6f6;
--color-neutral-100: #e7e7e7;
--color-neutral-200: #d1d1d1;
--color-neutral-300: #b0b0b0;
--color-neutral-400: #888888;
--color-neutral-500: #6d6d6d;
--color-neutral-600: #5d5d5d;
--color-neutral-700: #4f4f4f;
--color-neutral-800: #454545;
--color-neutral-900: #3d3d3d;
--color-neutral-950: #2b2b2b;
--color-success-100: #e0fbd8;
--color-success-200: #bcf8b0;
--color-success-300: #8eea85;
--color-success-400: #63d564;
--color-success-500: #35ba43;
--color-success-600: #269f3e;
--color-success-700: #1a8439;
--color-success-800: #106b32;
--color-success-900: #0b592f;
--color-info-100: #ccfcfe;
--color-info-200: #9bf3fd;
--color-info-300: #67e3fb;
--color-info-400: #42cdf8;
--color-info-500: #04acf3;
--color-info-600: #0185d0;
--color-info-700: #0264af;
--color-info-800: #01478d;
--color-info-900: #003375;
--color-warning-100: #fffcd3;
--color-warning-200: #fffaa9;
--color-warning-300: #fff67d;
--color-warning-400: #fff25d;
--color-warning-500: #ffed27;
--color-warning-600: #dbc91d;
--color-warning-700: #b7a714;
--color-warning-800: #93850b;
--color-warning-900: #7a6d07;
--color-danger-100: #ffe8da;
--color-danger-200: #ffcbb3;
--color-danger-300: #ffaa8d;
--color-danger-400: #ff8870;
--color-danger-500: #ff5242;
--color-danger-600: #da3030;
--color-danger-700: #b7212d;
--color-danger-800: #93152a;
--color-danger-900: #7a0c27;
--radius-none: 0px;
--radius-sm: 2px;
--radius-md: 4px;
--radius-lg: 8px;
--radius-xl: 12px;
--radius-2xl: 16px;
--radius-3xl: 24px;
--radius-full: 50%;
--font-bai-jamjuree: 'Bai Jamjuree', sans-serif;
--text-h1: 3.833rem;
/* ~46px */
--text-h2: 3.083rem;
/* ~37px */
--text-h3: 2.417rem;
/* ~29px */
--text-p1: 1.917rem;
/* ~23px */
--text-p2: 1.583rem;
/* ~19px */
--text-p3: 1.25rem;
/* 15px */
--text-label1: 1rem;
/* 12px */
--text-label2: 0.833rem;
/* ~10px */
--text-label3: 0.677rem;
/* ~8px */
}
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-neutral-200, currentColor);
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: var(--color-neutral-50);
}
::-webkit-scrollbar-thumb {
background: #cccccc;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-neutral-300);
}
html {
font-family: 'Bai Jamjuree', sans-serif;
font-weight: 400;
font-size: 12px;
line-height: 1.2;
}
}
+40
View File
@@ -0,0 +1,40 @@
import { createRoot } from 'react-dom/client';
import { middleware } from './middleware';
import { StrictMode } from 'react';
import { createBrowserRouter, RouteObject, RouterProvider } from 'react-router';
import {
add404PageToRoutesChildren,
addErrorElementToRoutes,
convertPagesToRoute,
QueryProvider,
} from '@imphnen-frontend-service/utils';
import './index.css';
const files = import.meta.glob('./app/**/*(page|layout).tsx');
const errorFiles = import.meta.glob('./app/**/*error.tsx');
const notFoundFiles = import.meta.glob('./app/**/*404.tsx');
const loadingFiles = import.meta.glob('./app/**/*loading.tsx');
const routes = convertPagesToRoute(files, loadingFiles) as RouteObject;
addErrorElementToRoutes(errorFiles, routes);
add404PageToRoutesChildren(notFoundFiles, routes);
const router = createBrowserRouter([
{
...routes,
loader: middleware,
shouldRevalidate: () => true,
},
]);
const rootElement = document.getElementById('root');
if (!rootElement) throw new Error('Failed to find the root element');
createRoot(rootElement).render(
<StrictMode>
<QueryProvider>
<RouterProvider router={router} />
</QueryProvider>
</StrictMode>
);
+11
View File
@@ -0,0 +1,11 @@
import { LoaderFunctionArgs } from 'react-router-dom';
export const middleware = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url);
const pathname = url.pathname;
if (pathname) return null;
return null;
};
+27
View File
@@ -0,0 +1,27 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts",
"vite/client"
]
},
"exclude": [
"src/**/*.spec.ts",
"src/**/*.test.ts",
"src/**/*.spec.tsx",
"src/**/*.test.tsx",
"src/**/*.spec.js",
"src/**/*.test.js",
"src/**/*.spec.jsx",
"src/**/*.test.jsx",
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts"
],
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
}
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"types": ["vite/client", "vitest"]
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../tsconfig.base.json"
}
+30
View File
@@ -0,0 +1,30 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts"
]
},
"include": [
"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"
]
}
+42
View File
@@ -0,0 +1,42 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
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/backoffice',
server: {
port: 3000,
host: 'localhost',
},
preview: {
port: 3001,
host: 'localhost',
},
plugins: [react(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
build: {
outDir: '../../dist/apps/backoffice',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},
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/backoffice',
provider: 'v8' as const,
},
},
}));
+2 -2
View File
@@ -6,8 +6,8 @@
<base href="/" /> <base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <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" /> <link rel="stylesheet" href="/src/index.css" />
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+5 -1
View File
@@ -1,5 +1,9 @@
import { join } from 'path';
export default { export default {
plugins: { plugins: {
'@tailwindcss/postcss': {}, '@tailwindcss/postcss': {
base: join(import.meta.dirname, '../../'),
},
}, },
}; };
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 MiB

@@ -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,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,23 @@
import { FC, ReactElement } from 'react';
import { RegisterResetBanner } from "@imphnen-frontend-service/ui/organisms";
import { ForgotStep, OtpForm } from "@imphnen-frontend-service/ui/molecules";
import { Button } from '@imphnen-frontend-service/ui/atoms';
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'>
<RegisterResetBanner />
<div className='border-2 border-primary-500/50 w-[596px] rounded-lg py-[70px] px-[48px] flex flex-col justify-center'>
<ForgotStep step={2} />
<h3 className='mt-5 text-3xl font-semibold text-primary-500'>Forgot Password</h3>
<h5 className='mt-2 text-xl font-medium text-primary-500'>Yeay~! Pesan dari dunia lain (a.k.a email-mu) sudah tiba!</h5>
<OtpForm />
<Button>Summon Password Baru!</Button>
</div>
</div>
</div>
);
};
export default Components;
@@ -0,0 +1,24 @@
import { FC, ReactElement } from 'react';
import { RegisterResetBanner } from "@imphnen-frontend-service/ui/organisms";
import { ForgotStep } from "@imphnen-frontend-service/ui/molecules";
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
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'>
<RegisterResetBanner />
<div className='border-2 border-primary-500/50 w-[596px] rounded-lg py-[70px] px-[48px] flex flex-col justify-center'>
<ForgotStep step={1} />
<h3 className='mt-5 text-3xl font-semibold text-primary-500'>Forgot Password</h3>
<h5 className='mt-2 text-xl font-medium text-primary-500'>Masukkan email-mu, dan biarkan kami memanggil password-mu kembali dari dunia lain!</h5>
<h6 className='text-gray-600 mt-8'>Email</h6>
<Input type='email' size='lg' placeholder='Contoh: yourname@mail.com'></Input>
<Button variant='primary' className='mt-7'>Kirim Kode OTP ^^</Button>
</div>
</div>
</div>
);
};
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,34 @@
import { FC, ReactElement } from 'react';
import { RegisterResetBanner } from "@imphnen-frontend-service/ui/organisms";
import { ForgotStep } from "@imphnen-frontend-service/ui/molecules";
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
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'>
<RegisterResetBanner />
<div className='border-2 border-primary-500/50 w-[596px] rounded-lg py-[70px] px-[48px] flex flex-col justify-center'>
<ForgotStep step={3} />
<h3 className='mt-5 text-3xl font-semibold text-primary-500'>Forgot Password</h3>
<h5 className='mt-2 text-md font-medium text-primary-500'>Sekarang, buatlah password baru yang lebih kuat, seperti jurus andalanmu!<span role="img" aria-label='emoji'>🔥</span></h5>
<div className='my-4'>
<h6 className='text-gray-700'>Password Baru</h6>
<Input className='w-full' type="password" placeholder='Buat password sekokoh armor legendary!'/>
<h6 className='text-gray-700 mt-5'>Ulang Password Baru</h6>
<Input className='w-full' type="password" placeholder='Pastikan Cocok! Jangan sampai ada typo, Senpai~!'/>
</div>
<div className='mb-4 text-gray-700 flex flex-col gap-3 text-sm'>
<h6><span role='img' aria-label='emoji'>💡</span> Tips dari kami:</h6>
<h6><span role='img' aria-label='emoji'> </span> Buat kombinasi huruf besar, kecil, angka, dan simbol untuk kekuatan maksimal!</h6>
<h6><span role='img' aria-label='emoji'></span> Pastikan kamu ingat password-mu atau simpan di tempat aman~</h6>
<h6>Masukkan password baru dan bersiaplah untuk kembali bertualang!</h6>
</div>
<Button className='mt-5'>Summon Password Baru!</Button>
</div>
</div>
</div>
);
};
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,73 @@
import { FC, ReactElement } from 'react';
import { 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';
export const Components: FC = (): ReactElement => {
const [searchParams] = useSearchParams();
const error = searchParams.get('error'); // Ambil nilai ?error=
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">
<LoginBanner />
<div className="border-2 border-primary-500/50 w-[596px] rounded-lg py-[70px] px-[96px] flex justify-center">
<div className="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">
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>
<div className="flex my-3 gap-3 justify-center">
<h5>Belum Punya akun ?</h5>
<a href="/auth/register" className="text-primary-500 font-medium">
Daftar Disini
</a>
</div>
<div className="flex items-center w-full">
<div className="flex-grow border-t border-blue-400 opacity-50"></div>
<span className="px-3 text-blue-400">Or</span>
<div className="flex-grow border-t border-blue-400 opacity-50"></div>
</div>
<Button
className="w-full my-3 text-gray-500 gap-2"
variant="secondary"
>
<p>Log In With Google</p>
<img
src="/image/33978ce5bed2da9bf9d73acad802182a.webp"
alt="Google Icon"
width={24}
/>
</Button>
</div>
</div>
</div>
</div>
);
};
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,19 @@
import { FC, ReactElement } from 'react';
import { RegisterResetBanner } from "@imphnen-frontend-service/ui/organisms";
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
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'>
<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>
</div>
</div>
);
};
export default Components;
+1
View File
@@ -1,5 +1,6 @@
@import url('https://fonts.googleapis.com/css2?family=Bai+Jamjuree:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Bai+Jamjuree:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&display=swap');
@import 'tailwindcss'; @import 'tailwindcss';
@source "../../../libs/ui/**/*.{ts,tsx}";
@theme { @theme {
--color-primary-50: #f0f8ff; --color-primary-50: #f0f8ff;
+2 -2
View File
@@ -8,11 +8,11 @@ export default defineConfig(() => ({
root: __dirname, root: __dirname,
cacheDir: '../../node_modules/.vite/apps/dimentorin', cacheDir: '../../node_modules/.vite/apps/dimentorin',
server: { server: {
port: 4200, port: 3000,
host: 'localhost', host: 'localhost',
}, },
preview: { preview: {
port: 4300, port: 3001,
host: 'localhost', host: 'localhost',
}, },
plugins: [react(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], plugins: [react(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
+2 -2
View File
@@ -6,8 +6,8 @@
<base href="/" /> <base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <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" /> <link rel="stylesheet" href="/src/index.css" />
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+5 -1
View File
@@ -1,5 +1,9 @@
import { join } from 'path';
export default { export default {
plugins: { plugins: {
'@tailwindcss/postcss': {}, '@tailwindcss/postcss': {
base: join(import.meta.dirname, '../../'),
},
}, },
}; };
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="47" height="62" viewBox="0 0 47 62" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.5 38.4999L9.19156 38.4166L8.5 38.4999ZM23.5 42.4999L24.0479 42.9301L23.5 42.4999ZM1.45141 0.804946C1.06764 0.831723 0.778242 1.16453 0.805019 1.5483L1.24135 7.80212C1.26813 8.18588 1.60094 8.47528 1.9847 8.4485C2.36847 8.42173 2.65787 8.08892 2.63109 7.70515L2.24324 2.1462L7.80219 1.75835C8.18595 1.73157 8.47535 1.39876 8.44858 1.015C8.4218 0.631233 8.08899 0.341836 7.70523 0.368611L1.45141 0.804946ZM44.8219 58.1179C44.6435 57.777 44.2226 57.6453 43.8818 57.8237C43.5409 58.002 43.4092 58.4229 43.5876 58.7638L44.8219 58.1179ZM39.979 53.323C40.2168 53.6254 40.6547 53.6778 40.9571 53.44C41.2596 53.2023 41.3119 52.7643 41.0742 52.4619L39.979 53.323ZM36.6126 47.4065C36.3439 47.1312 35.9029 47.126 35.6276 47.3947C35.3524 47.6635 35.3471 48.1045 35.6159 48.3797L36.6126 47.4065ZM30.8012 43.8589C31.0925 44.1102 31.5323 44.0779 31.7837 43.7866C32.035 43.4954 32.0026 43.0555 31.7114 42.8042L30.8012 43.8589ZM26.435 38.6311C26.1228 38.4063 25.6875 38.4771 25.4627 38.7893C25.2379 39.1015 25.3087 39.5368 25.6209 39.7616L26.435 38.6311ZM20.0706 36.2159C20.4077 36.4012 20.8312 36.2782 21.0166 35.9411C21.2019 35.604 21.0789 35.1804 20.7418 34.9951L20.0706 36.2159ZM14.4146 32.3979C14.0394 32.313 13.6664 32.5483 13.5814 32.9235C13.4965 33.2987 13.7318 33.6717 14.107 33.7566L14.4146 32.3979ZM9.34382 35.388C9.46205 35.0219 9.26113 34.6293 8.89505 34.5111C8.52897 34.3928 8.13636 34.5938 8.01813 34.9598L9.34382 35.388ZM8.36062 41.3331C8.46936 41.7021 8.85665 41.9131 9.22566 41.8044C9.59467 41.6957 9.80567 41.3084 9.69693 40.9394L8.36062 41.3331ZM12.2528 45.0931C11.9511 44.8544 11.513 44.9055 11.2743 45.2072C11.0357 45.5089 11.0867 45.9469 11.3884 46.1856L12.2528 45.0931ZM17.0187 47.5227C17.3975 47.4557 17.6503 47.0943 17.5833 46.7155C17.5163 46.3367 17.1549 46.0839 16.776 46.1509L17.0187 47.5227ZM21.1929 43.9203C20.9014 44.1714 20.8687 44.6112 21.1197 44.9027C21.3707 45.1942 21.8106 45.227 22.1021 44.9759L21.1929 43.9203ZM25.5435 39.9375C25.6453 39.5665 25.4271 39.1833 25.0561 39.0815C24.6851 38.9797 24.3019 39.1979 24.2001 39.5688L25.5435 39.9375ZM24.0968 33.7918C24.1836 34.1665 24.5578 34.4 24.9326 34.3132C25.3074 34.2264 25.5408 33.8522 25.454 33.4774L24.0968 33.7918ZM23.2992 27.5399C23.134 27.1924 22.7185 27.0447 22.371 27.2098C22.0236 27.375 21.8758 27.7905 22.041 28.138L23.2992 27.5399ZM19.0765 22.8124C19.2817 23.1378 19.7118 23.2353 20.0372 23.0302C20.3627 22.825 20.4602 22.3949 20.255 22.0695L19.0765 22.8124ZM16.713 16.9396C16.4827 16.6315 16.0462 16.5684 15.738 16.7988C15.4299 17.0291 15.3669 17.4656 15.5972 17.7738L16.713 16.9396ZM11.7675 12.9776C12.0165 13.2708 12.4561 13.3068 12.7493 13.0579C13.0426 12.8089 13.0786 12.3694 12.8297 12.0761L11.7675 12.9776ZM8.67476 7.44825C8.40998 7.16917 7.96909 7.15758 7.69002 7.42236C7.41094 7.68714 7.39935 8.12803 7.66413 8.4071L8.67476 7.44825ZM3.3149 4.07865C3.59572 4.34158 4.03652 4.32709 4.29945 4.04628C4.56239 3.76546 4.5479 3.32466 4.26708 3.06173L3.3149 4.07865ZM46.1653 61.2935C45.8476 60.2699 45.391 59.2055 44.8219 58.1179L43.5876 58.7638C44.1267 59.7941 44.5472 60.78 44.8347 61.7065L46.1653 61.2935ZM41.0742 52.4619C39.7433 50.7694 38.2336 49.0665 36.6126 47.4065L35.6159 48.3797C37.2071 50.0093 38.6831 51.6749 39.979 53.323L41.0742 52.4619ZM31.7114 42.8042C29.9811 41.3111 28.202 39.9036 26.435 38.6311L25.6209 39.7616C27.353 41.0089 29.1002 42.391 30.8012 43.8589L31.7114 42.8042ZM20.7418 34.9951C18.4611 33.7412 16.2968 32.824 14.4146 32.3979L14.107 33.7566C15.8134 34.143 17.8499 34.995 20.0706 36.2159L20.7418 34.9951ZM8.01813 34.9598C7.70205 35.9385 7.63559 37.1473 7.80843 38.5831L9.19156 38.4166C9.0331 37.1002 9.11052 36.1103 9.34382 35.388L8.01813 34.9598ZM7.80843 38.5831C7.92897 39.5845 8.1153 40.5006 8.36062 41.3331L9.69693 40.9394C9.47576 40.1888 9.30385 39.3495 9.19156 38.4166L7.80843 38.5831ZM11.3884 46.1856C13.025 47.4803 15.0176 47.8768 17.0187 47.5227L16.776 46.1509C15.1188 46.4441 13.5408 46.1121 12.2528 45.0931L11.3884 46.1856ZM22.1021 44.9759C22.7906 44.3829 23.4445 43.6986 24.0479 42.9301L22.9521 42.0698C22.4024 42.7699 21.8106 43.3884 21.1929 43.9203L22.1021 44.9759ZM24.0479 42.9301C24.7553 42.029 25.2456 41.0232 25.5435 39.9375L24.2001 39.5688C23.9482 40.4864 23.5385 41.3229 22.9521 42.0698L24.0479 42.9301ZM25.454 33.4774C25.0137 31.5767 24.2681 29.5782 23.2992 27.5399L22.041 28.138C22.9794 30.1121 23.685 32.0139 24.0968 33.7918L25.454 33.4774ZM20.255 22.0695C19.1723 20.352 17.978 18.6317 16.713 16.9396L15.5972 17.7738C16.8433 19.4405 18.0161 21.1303 19.0765 22.8124L20.255 22.0695ZM12.8297 12.0761C11.464 10.467 10.0656 8.91422 8.67476 7.44825L7.66413 8.4071C9.038 9.85515 10.4191 11.3889 11.7675 12.9776L12.8297 12.0761ZM4.26708 3.06173C3.47703 2.32198 2.70419 1.62396 1.95695 0.974187L1.04282 2.02544C1.77631 2.66325 2.53646 3.34977 3.3149 4.07865L4.26708 3.06173Z" fill="#81CBF8"/>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="79" height="88" viewBox="0 0 79 88" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.00004 87.8788C0.5147 87.8788 0.121257 87.4853 0.121261 87C0.121265 86.5147 0.514715 86.1212 1.00006 86.1212L1.00004 87.8788ZM43.4999 69.5L44.1213 70.1214L43.4999 69.5ZM74.1915 0.677276C74.6459 0.506859 75.1525 0.737104 75.3229 1.19154L78.1 8.59705C78.2704 9.05149 78.0401 9.55803 77.5857 9.72845C77.1313 9.89887 76.6247 9.66862 76.4543 9.21418L73.9858 2.6315L67.4031 5.10001C66.9487 5.27042 66.4421 5.04018 66.2717 4.58574C66.1013 4.1313 66.3316 3.62475 66.786 3.45434L74.1915 0.677276ZM4.9001 86.026C5.38484 86.0017 5.79745 86.375 5.8217 86.8598C5.84595 87.3445 5.47266 87.7571 4.98792 87.7814L4.9001 86.026ZM12.9278 86.921C12.4495 87.0033 11.9951 86.6822 11.9128 86.2039C11.8306 85.7255 12.1517 85.2711 12.63 85.1889L12.9278 86.921ZM20.1617 83.3426C20.6234 83.1929 21.119 83.4458 21.2687 83.9075C21.4184 84.3691 21.1655 84.8648 20.7038 85.0145L20.1617 83.3426ZM28.1156 81.9688C27.682 82.187 27.1537 82.0124 26.9355 81.5789C26.7174 81.1453 26.8919 80.617 27.3255 80.3988L28.1156 81.9688ZM33.976 76.4152C34.3737 76.1371 34.9216 76.234 35.1998 76.6318C35.4779 77.0295 35.381 77.5774 34.9832 77.8556L33.976 76.4152ZM41.227 72.8711C40.8664 73.1959 40.3107 73.1668 39.9859 72.8061C39.6611 72.4455 39.6902 71.8898 40.0508 71.5651L41.227 72.8711ZM45.7562 65.3879C46.0375 64.9923 46.5861 64.8997 46.9816 65.181C47.3771 65.4623 47.4697 66.011 47.1885 66.4065L45.7562 65.3879ZM52.0891 58.2652C51.8619 58.6941 51.3301 58.8576 50.9012 58.6304C50.4723 58.4033 50.3087 57.8715 50.5359 57.4426L52.0891 58.2652ZM54.6326 49.0687C54.8345 48.6273 55.3559 48.4332 55.7973 48.6351C56.2386 48.837 56.4328 49.3584 56.2309 49.7998L54.6326 49.0687ZM59.9926 41.1685C59.8055 41.6163 59.2908 41.8277 58.843 41.6407C58.3952 41.4536 58.1837 40.9389 58.3708 40.4911L59.9926 41.1685ZM61.8824 31.8101C62.06 31.3584 62.5702 31.1363 63.0218 31.3139C63.4735 31.4915 63.6956 32.0017 63.518 32.4533L61.8824 31.8101ZM66.8965 23.6846C66.7245 24.1385 66.2172 24.367 65.7634 24.1951C65.3095 24.0232 65.0809 23.5159 65.2529 23.062L66.8965 23.6846ZM68.5503 14.2814C68.7204 13.8269 69.2268 13.5963 69.6814 13.7664C70.1359 13.9366 70.3665 14.443 70.1964 14.8975L68.5503 14.2814ZM73.5253 6.13925C73.3477 6.59092 72.8375 6.81308 72.3859 6.63546C71.9342 6.45784 71.712 5.94769 71.8897 5.49603L73.5253 6.13925ZM1.00006 86.1212C2.32119 86.1212 3.62073 86.09 4.9001 86.026L4.98792 87.7814C3.67776 87.8469 2.34888 87.8788 1.00004 87.8788L1.00006 86.1212ZM12.63 85.1889C15.2148 84.7445 17.7207 84.1341 20.1617 83.3426L20.7038 85.0145C18.1788 85.8332 15.591 86.4632 12.9278 86.921L12.63 85.1889ZM27.3255 80.3988C29.583 79.2627 31.7958 77.9399 33.976 76.4152L34.9832 77.8556C32.7362 79.427 30.451 80.7935 28.1156 81.9688L27.3255 80.3988ZM40.0508 71.5651C40.9971 70.7129 41.9393 69.8178 42.8785 68.8786L44.1213 70.1214C43.1613 71.0814 42.1969 71.9976 41.227 72.8711L40.0508 71.5651ZM42.8785 68.8786C43.8057 67.9514 44.7678 66.7776 45.7562 65.3879L47.1885 66.4065C46.1625 67.8491 45.1379 69.1048 44.1213 70.1214L42.8785 68.8786ZM50.5359 57.4426C51.9015 54.864 53.273 52.0408 54.6326 49.0687L56.2309 49.7998C54.861 52.7943 53.4745 55.6493 52.0891 58.2652L50.5359 57.4426ZM58.3708 40.4911C59.5708 37.6181 60.7458 34.7001 61.8824 31.8101L63.518 32.4533C62.378 35.3521 61.1983 38.2818 59.9926 41.1685L58.3708 40.4911ZM65.2529 23.062C66.4246 19.9686 67.5297 17.0081 68.5503 14.2814L70.1964 14.8975C69.1765 17.6222 68.0698 20.5871 66.8965 23.6846L65.2529 23.062ZM71.8897 5.49603C72.5771 3.74785 73.1856 2.26834 73.7 1.13646L75.3001 1.86375C74.8047 2.95358 74.2095 4.39945 73.5253 6.13925L71.8897 5.49603Z" fill="#81CBF8"/>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 351 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 511 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="283" height="222" viewBox="0 0 283 222" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M41.2124 71.9365L30.3686 94.0759L1 115.312L8.2292 133.385L15.0066 149.65L26.3022 157.783L30.3686 179.471L35.7905 221.491L73.7438 218.328L104.92 214.713L124.8 221.491L181.278 218.328L192.574 210.195L205.225 214.713C213.207 214.864 229.894 215.075 232.786 214.713C235.678 214.352 241.22 219.081 243.63 221.491H263.962L276.613 196.64L281.583 166.368L272.999 154.169L263.962 143.325L254.926 133.385L235.949 127.963V121.185C236.702 116.065 238.298 105.1 238.66 102.209C239.021 99.3171 245.437 90.1601 248.6 85.9431C249.805 77.9608 252.215 61.5445 252.215 59.7372V41.2124L248.6 18.1693L243.63 6.87372L225.105 1L205.225 3.25912L192.574 10.4883C188.809 14.4041 181.278 22.6876 181.278 24.4949C181.278 26.3022 174.049 36.3929 170.434 41.2124L160.042 36.6942L127.511 24.4949L99.046 18.1693H53.8635L26.3022 27.6577V43.4715L30.3686 67.4183L41.2124 71.9365Z" fill="white" stroke="#E7E7E7" stroke-width="0.90365"/>
</svg>

After

Width:  |  Height:  |  Size: 1017 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="304" height="200" viewBox="0 0 304 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.5132 38.0564L0.729248 148.302V175.411C13.531 178.725 40.038 185.351 43.6526 185.351C47.2672 185.351 66.8463 187.761 76.184 188.966L92.9015 167.278H112.33H134.469C137.632 167.278 151.639 173.152 160.675 175.411C169.712 177.67 181.911 183.544 188.688 188.966C195.466 194.388 199.984 193.032 205.858 197.551C210.557 201.165 224.985 199.057 231.612 197.551H258.721L284.023 188.966C288.542 183.243 297.578 170.893 297.578 167.278V139.717V105.378L303 64.7141L291.704 38.0564L278.602 32.6345L265.047 21.7907H244.263L231.612 2.36227C226.943 1.1574 216.792 -0.529408 213.539 2.36227C209.472 5.97687 187.785 2.36227 177.845 2.36227C167.904 2.36227 147.572 8.23599 142.602 11.8506C138.626 14.7423 120.764 19.6822 112.33 21.7907L76.184 8.23599L34.1643 2.36227L21.5132 38.0564Z" fill="white" stroke="#E7E7E7" stroke-width="0.90365"/>
</svg>

After

Width:  |  Height:  |  Size: 941 B

@@ -0,0 +1,148 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import {
InputField,
Modal,
Stepper,
} from '@imphnen-frontend-service/ui/molecules';
import { useQueryState } from '@imphnen-frontend-service/utils';
import { Fragment } from 'react/jsx-runtime';
interface IModalFormForgotPasswordProps {
isOpen: boolean;
onClose: () => void;
}
const ModalFormForgotPassword = ({
isOpen,
onClose,
}: IModalFormForgotPasswordProps) => {
const {
step: currentStep,
nextStep,
prevStep,
resetStep,
} = useQueryState('step', {
defaultValue: 1,
maxValue: 3,
minValue: 1,
});
return (
<Modal
className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7"
isOpen={isOpen}
onClose={() => {
onClose();
resetStep();
}}
>
<Modal.Header className="space-y-4">
<img src="/logos/logo.svg" alt="" className="h-[70px] w-auto" />
<h1 className="text-primary-500 text-p1 text-center font-semibold">
Forgot Password
</h1>
<Stepper currentStep={currentStep} totalSteps={3} />
</Modal.Header>
<Modal.Content className="space-y-6">
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
{currentStep === 2 && (
<StepTwo nextStep={nextStep} prevStep={prevStep} />
)}
{currentStep === 3 && (
<StepThree onClose={onClose} resetStep={resetStep} />
)}
</Modal.Content>
</Modal>
);
};
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
}
const StepOne = ({ nextStep, onClose }: IStepOneProps) => (
<>
<InputField
label="Email"
placeholder="Masukkan Email yang Terdaftar"
type="email"
size="lg"
className="w-full"
/>
<div className="flex gap-6">
<Button variant="bordered" size="md" className="w-full" onClick={onClose}>
Back To Login
</Button>
<Button size="md" className="w-full" onClick={nextStep}>
Kirim OTP
</Button>
</div>
</>
);
interface IStepTwoProps {
nextStep: () => void;
prevStep: () => void;
}
const StepTwo = ({ nextStep, prevStep }: IStepTwoProps) => (
<Fragment>
<InputField
label="Kode OTP"
placeholder="Masukkan Kode OTP"
type="text"
size="lg"
className="w-full"
/>
<div className="flex gap-6">
<Button
size="md"
variant="bordered"
className="w-full"
onClick={prevStep}
>
Change Email
</Button>
<Button size="md" className="w-full" onClick={nextStep}>
Reset Password
</Button>
</div>
</Fragment>
);
interface IStepThreeProps {
onClose: () => void;
resetStep: () => void;
}
const StepThree = ({ onClose, resetStep }: IStepThreeProps) => (
<>
<InputField
label="Password Baru"
placeholder="Masukkan Password Baru"
type="password"
size="lg"
className="w-full"
/>
<InputField
label="Ulang Password"
placeholder="Masukkan Ulang Password"
type="password"
size="lg"
className="w-full"
/>
<Button
size="md"
className="w-full"
onClick={() => {
console.log('Password reset submitted');
onClose();
resetStep();
}}
>
Buat Password Baru
</Button>
</>
);
export default ModalFormForgotPassword;
@@ -0,0 +1,77 @@
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 { useLogin } from '../../_hooks/use-login';
interface IModalFormLogin {
isOpen: boolean;
onClose: () => void;
onForgotPassword: () => void;
setIsOpenRegisterModal: (value: boolean) => void;
}
const ModalFormLogin = ({
isOpen,
onClose,
onForgotPassword,
setIsOpenRegisterModal,
}: IModalFormLogin) => {
const { form, onSubmit } = useLogin();
return (
<Modal
className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7"
isOpen={isOpen}
onClose={onClose}
>
<Modal.Header className="space-y-4">
<img src="/logos/logo.svg" alt="" className="h-[70px] w-auto" />
<h1 className="text-primary-500 text-p1 text-center font-semibold">
Login
</h1>
</Modal.Header>
<Modal.Content>
<form className="space-y-4" onSubmit={onSubmit}>
<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"
/>
<h1 className="text-end text-primary-500 text-xl font-medium">
<Button variant="text" onClick={onForgotPassword}>
Lupa Password?
</Button>
</h1>
<Button type="submit" size="md" className="w-full">
Login
</Button>
<div className="flex justify-center gap-2 pt-2.5 font-medium">
<p className="text-neutral-500">Belum punya akun?</p>
<Button
variant="text"
className="text-primary-500 hover:text-primary-600 m-0 p-0"
onClick={() => setIsOpenRegisterModal(true)}
>
Daftar
</Button>
</div>
</form>
</Modal.Content>
</Modal>
);
};
export default ModalFormLogin;
@@ -0,0 +1,180 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
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;
onClose: () => void;
}
const ModalFormRegister = ({ isOpen, onClose }: IModalFormRegisterProps) => {
const { form, onSubmit, isStepOneValid } = useRegister();
const {
step: currentStep,
nextStep,
prevStep,
resetStep,
} = useQueryState('registerStep', {
defaultValue: 1,
maxValue: 2,
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"
isOpen={isOpen}
onClose={() => {
onClose();
resetStep();
}}
>
<Modal.Header className="space-y-4">
<img src="/logos/logo.svg" alt="" className="h-[70px] w-auto" />
<h1 className="text-primary-500 text-p1 text-center font-semibold">
Register
</h1>
<h2 className="text-neutral-500 text-lg text-center">
{currentStep === 1 ? 'Info Akun' : 'Informasi Pengiriman Hadiah'}
</h2>
</Modal.Header>
<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;
isStepOneValid: boolean;
}
const StepOne = ({ form, nextStep, isStepOneValid }: IStepOneProps) => {
return (
<>
<ControlledInputField
control={form.control}
name="fullname"
label="Nama Lengkap"
placeholder="Masukkan Nama Lengkap"
type="text"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Email"
placeholder="Masukkan Email Anda"
type="email"
name="email"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
name="password"
label="Password"
placeholder="Masukkan Password"
type="password"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
name="confirm_password"
label="Ulangi Password"
placeholder="Masukkan Ulang Password"
type="password"
size="lg"
className="w-full"
/>
<Button
size="md"
className="w-full"
onClick={nextStep}
disabled={!isStepOneValid}
>
Selanjutnya
</Button>
</>
);
};
interface IStepTwoProps {
form: UseFormReturn<TRegisterRequest, any, TRegisterRequest>;
prevStep: () => void;
}
const StepTwo = ({ form, prevStep }: IStepTwoProps) => (
<>
<ControlledInputField
control={form.control}
name="phone_number"
label="Nomor Telepon"
placeholder="Masukkan Nomor Telepon Aktif"
type="text"
size="lg"
className="w-full"
/>
<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"
/>
<div className="flex gap-6">
<Button size="md" variant="text" className="w-[40%]" onClick={prevStep}>
Kembali
</Button>
<Button size="md" className="w-full" type="submit">
Gacha Sekarang
</Button>
</div>
</>
);
export default ModalFormRegister;
@@ -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<TGachaItem> = ({
src,
label,
className,
}): ReactElement => {
return (
<div className="snap-center flex-auto justify-center items-center flex flex-col min-w-full overflow-hidden">
<div className="max-h-[180px] md:max-h-[270px] md:max-w-[500px] mb-2 md:mb-5 flex flex-1 justify-center items-center">
<img
src={src}
alt="Banner"
width={255}
height={255}
className={cn('h-[150px] md:h-[255px] object-contain', className)}
/>
</div>
<p className="font-semibold text-center md:text-p2">{label}</p>
</div>
);
};
+26
View File
@@ -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<TLoginRequest>({
resolver: zodResolver(authLoginSchema),
mode: 'all',
});
const onSubmit = form.handleSubmit((data) => {
postLogin.mutate(data, {
onSuccess: () => console.log('Success Login'),
});
});
return {
form,
onSubmit,
};
};
+58
View File
@@ -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,
};
};
+9 -7
View File
@@ -1,14 +1,16 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
import { Navbar } from '@imphnen-frontend-service/ui/organisms'; 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 => { export const AppLayout: FC = (): ReactElement => {
return ( return (
<main className="bg-primary-50 min-h-screen px-20 py-[60px]"> <ModalLoginProvider>
<Navbar /> <main className="bg-primary-50 min-h-screen">
<Outlet /> <Navbar />
</main> <Outlet />
</main>
</ModalLoginProvider>
); );
}; };
export default AppLayout; export default AppLayout;
+206 -2
View File
@@ -1,7 +1,211 @@
import { FC, ReactElement } from 'react'; import { ArrowDownOutlined } from '@ant-design/icons';
import { Button } from '@imphnen-frontend-service/ui/atoms';
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';
import { GachaItem } from './_components/item/gacha-item';
import { useModalLogin } from '@imphnen-frontend-service/utils';
export const Components: FC = (): ReactElement => { export const Components: FC = (): ReactElement => {
return <>Hallo</>; const { showModalLogin, setShowModalLogin } = useModalLogin();
const [showModalForgotPassword, setShowModalForgotPassword] = useState(false);
const [showModalRegister, setShowModalRegister] = useState(false);
const scrollToRoulette = () => {
const rouletteSection = document.getElementById('roulette');
if (rouletteSection) {
rouletteSection.scrollIntoView({ behavior: 'smooth' });
}
};
const handleForgotPasswordClick = () => {
setShowModalLogin(false);
setShowModalForgotPassword(true);
};
return (
<Fragment>
<section
id="landing-page"
className="my-12 md:my-24 grid grid-cols-4 md:grid-cols-8 lg:grid-cols-12 justify-items-center max-w-[1280px] items-center justify-center mx-[32px] md:mx-[60px] lg:mx-[80px] xl:mx-auto"
>
<div className="order-none lg:order-1 flex flex-col col-span-4 md:col-span-8 lg:col-span-4 md:w-full lg:w-fit items-center gap-7 md:gap-13 text-primary-500 mb-10 md:mb-20">
<img
src="/logos/logo.svg"
alt="IMPHNEN Logo"
className="w-[170px] md:w-[317px]"
/>
<div className="font-semibold text-center">
<div className="text-p1 md:text-h2 lg:font-bold text-center">
<p>New Merchandise</p>
<p>Coming UP</p>
</div>
<div className="text-base md:text-p2 mt-3">
Periode Gacha: 1 - 31 Maret 2025
</div>
</div>
<div className="text-center font-semibold text-p3 md:text-p1">
<p>Let's Go Checkout Our Merch &</p>
<p>Gacha Your Prize Here</p>
</div>
<Button
size="sm"
variant="bordered"
className="flex gap-3 px-3 md:px-5 py-2 md:py-4 bg-primary-100 border border-primary-500 border-dashed rounded md:rounded-md animate-bounce text-label2 md:text-p2"
onClick={scrollToRoulette}
>
<ArrowDownOutlined />
<span>Scroll For Gacha</span>
<ArrowDownOutlined />
</Button>
</div>
<div className="order-1 lg:order-none flex flex-col col-span-4 justify-center items-center mb-8 relative">
<div className="relative h-[150px] md:h-[230px]">
<img
src="/merch/1.png"
alt="Merch 1"
className="relative top-[3px] md:top-[6px] left-[4px] md:left-[7px] z-10 w-[165px] md:w-[266px]"
/>
<img
className="absolute top-0 left-0 min-w-[174px] md:min-w-[280px] z-0"
src="/merch/Vector-1.svg"
alt=""
/>
<img
className="absolute hidden lg:block -bottom-[50px] -right-[50px]"
src="/landing-arrow-1.svg"
alt=""
/>
</div>
<div className="px-3 py-1 text-primary-500 font-semibold text-base md:text-p2 bg-white shadow-md rounded">
Official Merch IMPHNEN
</div>
<div className="mt-2 text-primary-500 font-bold text-p2 md:text-h3">
~ 175k ~
</div>
</div>
<div className="order-2 flex flex-col col-span-4 justify-center items-center mb-8 relative overflow-hidden">
<div className="relative h-[130px] md:h-[230px]">
<img
width={400}
height={400}
src="/merch/2.png"
alt="Merch 2"
className="relative top-[5px] md:top-[8px] left-0 md:left-[5px] z-10 w-[165px] md:w-[277px]"
/>
<img
width={400}
height={400}
className="absolute top-0 -left-[5px] md:-left-[8px] z-0 min-w-[174px] md:min-w-[302px]"
src="/merch/Vector-2.svg"
alt="Merch 3"
/>
<img
width={400}
height={400}
className="absolute hidden lg:block -bottom-[100px] -left-[55px]"
src="/landing-arrow-2.svg"
alt="Merch 1"
/>
</div>
<div className="px-3 py-1 text-primary-500 font-semibold text-base md:text-p2 bg-white shadow-md rounded">
IMPHNEN Mini Merch
</div>
<div className="mt-2 text-primary-500 font-bold text-p2 md:text-h3">
~ 90k ~
</div>
</div>
</section>
<section
id="roulette"
className="mt-20 pb-50 lg:py-90 mx-[32px] md:mx-[60px] lg:mx-[80px] xl:mx-auto lg:max-w-[1280px] grid grid-cols-4 md:grid-cols-8 lg:grid-cols-12 justify-items-center gap-y-16 md:gap-y-28"
>
<div className="col-span-4 md:col-span-8 lg:col-span-6 text-primary-500 bg-white rounded-lg md:rounded-xl p-5 md:py-9 md:px-13 shadow md:max-w-[430px]">
<header className="mb-4">
<h3 className="font-semibold text-p3 md:text-h2">Gacha Roulette</h3>
</header>
<h4 className="font-semibold my-2 text-base md:text-p1">
How to participate
</h4>
<p className="text-base md:text-p2">
Kamu harus melakukan pembelian merch batch 2 di shopee IMPHNEN
</p>
<h4 className="font-semibold my-2 text-base md:text-p1">
How to gacha
</h4>
<ol className="text-base md:text-p2 list-decimal list-outside ml-4 md:ml-6">
<li>First, press the "Spin Now" button</li>
<li>
If the merch does not match you can reroll by paying IDR5,000
</li>
<li>If it is appropriate you can submit</li>
<li>Then fill in the data for your merch delivery process</li>
</ol>
</div>
<div className="col-span-4 md:col-span-8 lg:col-span-6 flex flex-col items-center gap-4 md:gap-8 overflow-x-hidden">
<div className="bg-white text-primary-500 font-medium text-p3 md:text-h3 shadow py-2 px-4 md:py-4 md:px-8 max-w-fit rounded-md md:rounded-lg">
Here Take Your Prize
</div>
<section
id="gacha-play"
className="flex flex-nowrap overflow-auto w-full gap-x-8 snap-x snap-mandatory"
>
<GachaItem
src="/gacha/certificate.png"
label="Sertifikat + Laminating"
/>
<GachaItem
src="/gacha/lanyard-id-card.png"
label="Lanyard + ID Card"
/>
<GachaItem src="/gacha/pin.png" label="Pin" />
<GachaItem src="/gacha/sticker.png" label="Sticker Isi 3" />
<GachaItem src="/gacha/sticker.png" label="Sticker Isi 5" />
<GachaItem
src="/gacha/gelang-karet.png"
label="Gelang Karet"
className="h-[86px] md:h-[160px]"
/>
</section>
<Button variant="secondary" size="md">
Spin Now
</Button>
</div>
</section>
<div className="sticky bottom-0 h-[86px] md:h-[200px] bg-gradient-to-b from-primary-500/0 to-primary-500/50 to-80%"></div>
<ModalFormLogin
isOpen={showModalLogin}
onClose={() => setShowModalLogin(false)}
onForgotPassword={handleForgotPasswordClick}
setIsOpenRegisterModal={setShowModalRegister}
key="login"
/>
<ModalFormRegister
isOpen={showModalRegister}
onClose={() => {
setShowModalRegister(false);
}}
key="register"
/>
<ModalFormForgotPassword
isOpen={showModalForgotPassword}
onClose={() => {
setShowModalForgotPassword(false);
setShowModalLogin(true);
}}
key="forgot-password"
/>
</Fragment>
);
}; };
export default Components; export default Components;
View File
+79 -63
View File
@@ -1,72 +1,71 @@
@import url('https://fonts.googleapis.com/css2?family=Bai+Jamjuree:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Bai+Jamjuree:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&display=swap');
@import 'tailwindcss'; @import 'tailwindcss';
@source "../../../libs/ui/**/*.{ts,tsx}";
@theme { @theme {
--color-primary-50: #f0f8ff; --color-primary-50: #f0f8ff;
--color-primary-100: #e1f0fd; --color-primary-100: #e1f0fd;
--color-primary-200: #b8e1f8; --color-primary-200: #bce1fb;
--color-primary-300: #a1d4f0; --color-primary-300: #81cbf8;
--color-primary-400: #4aa4da; --color-primary-400: #3eb0f2;
--color-primary-500: #1a8ce6; --color-primary-500: #23a1eb;
--color-primary-600: #1673c7; --color-primary-600: #0877c1;
--color-primary-700: #0f5ca5; --color-primary-700: #085f9c;
--color-primary-800: #0a4780; --color-primary-800: #0b5181;
--color-primary-900: #04305d; --color-primary-900: #0f446b;
--color-primary-950: #0a2b47;
--color-neutral-50: #f7f7f7; --color-neutral-50: #f6f6f6;
--color-neutral-100: #e7e7e7; --color-neutral-100: #e7e7e7;
--color-neutral-200: #d0d0d0; --color-neutral-200: #d1d1d1;
--color-neutral-300: #b7b7b7; --color-neutral-300: #b0b0b0;
--color-neutral-400: #9f9f9f; --color-neutral-400: #888888;
--color-neutral-500: #7e7e7e; --color-neutral-500: #6d6d6d;
--color-neutral-600: #5d5d5d; --color-neutral-600: #5d5d5d;
--color-neutral-700: #4a4a4a; --color-neutral-700: #4f4f4f;
--color-neutral-800: #3a3a3a; --color-neutral-800: #454545;
--color-neutral-900: #2f2f2f; --color-neutral-900: #3d3d3d;
--color-neutral-950: #2b2b2b;
--color-success-50: #e7f2ee; --color-success-100: #e0fbd8;
--color-success-100: #cde6dd; --color-success-200: #bcf8b0;
--color-success-200: #9bccbc; --color-success-300: #8eea85;
--color-success-300: #78bdab; --color-success-400: #63d564;
--color-success-400: #4ca88f; --color-success-500: #35ba43;
--color-success-500: #349078; --color-success-600: #269f3e;
--color-success-600: #2f7c66; --color-success-700: #1a8439;
--color-success-700: #26624f; --color-success-800: #106b32;
--color-success-800: #1e4a3b; --color-success-900: #0b592f;
--color-success-900: #1b513b;
--color-info-50: #e7f4f5; --color-info-100: #ccfcfe;
--color-info-100: #cce9ea; --color-info-200: #9bf3fd;
--color-info-200: #99d3d5; --color-info-300: #67e3fb;
--color-info-300: #78c4c7; --color-info-400: #42cdf8;
--color-info-400: #3aa9ad; --color-info-500: #04acf3;
--color-info-500: #279599; --color-info-600: #0185d0;
--color-info-600: #207d82; --color-info-700: #0264af;
--color-info-700: #1a666b; --color-info-800: #01478d;
--color-info-800: #124c52; --color-info-900: #003375;
--color-info-900: #093d43;
--color-warning-50: #fffce6; --color-warning-100: #fffcd3;
--color-warning-100: #fff8cc; --color-warning-200: #fffaa9;
--color-warning-200: #fef39b; --color-warning-300: #fff67d;
--color-warning-300: #fde967; --color-warning-400: #fff25d;
--color-warning-400: #fbd93d; --color-warning-500: #ffed27;
--color-warning-500: #f3c215; --color-warning-600: #dbc91d;
--color-warning-600: #d7a20f; --color-warning-700: #b7a714;
--color-warning-700: #aa7e0c; --color-warning-800: #93850b;
--color-warning-800: #805c07; --color-warning-900: #7a6d07;
--color-warning-900: #665c00;
--color-danger-50: #ffe7e7; --color-danger-100: #ffe8da;
--color-danger-100: #ffcece; --color-danger-200: #ffcbb3;
--color-danger-200: #ff9f9f; --color-danger-300: #ffaa8d;
--color-danger-300: #ff6e6e; --color-danger-400: #ff8870;
--color-danger-400: #fa4646; --color-danger-500: #ff5242;
--color-danger-500: #ef1f1f; --color-danger-600: #da3030;
--color-danger-600: #d11515; --color-danger-700: #b7212d;
--color-danger-700: #a51111; --color-danger-800: #93152a;
--color-danger-800: #7f0c0c; --color-danger-900: #7a0c27;
--color-danger-900: #5d2d2d;
--radius-none: 0px; --radius-none: 0px;
--radius-sm: 2px; --radius-sm: 2px;
@@ -79,16 +78,28 @@
--font-bai-jamjuree: 'Bai Jamjuree', sans-serif; --font-bai-jamjuree: 'Bai Jamjuree', sans-serif;
--text-h1: 3rem; /* ~48px */ --text-h1: 3.833rem;
--text-h2: 2.4rem; /* ~38.4px */ /* ~46px */
--text-h3: 1.92rem; /* ~30.72px */ --text-h2: 3.083rem;
--text-p1: 1.54rem; /* ~24.58px */ /* ~37px */
--text-p2: 1.23rem; /* ~19.68px */ --text-h3: 2.417rem;
--text-label1: 0.98rem; /* ~15.74px */ /* ~29px */
--text-label2: 0.78rem; /* ~12.59px */ --text-p1: 1.917rem;
/* ~23px */
--text-p2: 1.583rem;
/* ~19px */
--text-p3: 1.25rem;
/* 15px */
--text-label1: 1rem;
/* 12px */
--text-label2: 0.833rem;
/* ~10px */
--text-label3: 0.677rem;
/* ~8px */
} }
@layer base { @layer base {
*, *,
::after, ::after,
::before, ::before,
@@ -102,13 +113,16 @@
width: 6px; width: 6px;
height: 6px; height: 6px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: var(--color-neutral-50); background: var(--color-neutral-50);
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #cccccc; background: #cccccc;
border-radius: 3px; border-radius: 3px;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: var(--color-neutral-300); background: var(--color-neutral-300);
} }
@@ -116,5 +130,7 @@
html { html {
font-family: 'Bai Jamjuree', sans-serif; font-family: 'Bai Jamjuree', sans-serif;
font-weight: 400; font-weight: 400;
font-size: 12px;
line-height: 1.2;
} }
} }
+2
View File
@@ -8,6 +8,7 @@ import {
convertPagesToRoute, convertPagesToRoute,
QueryProvider, QueryProvider,
} from '@imphnen-frontend-service/utils'; } from '@imphnen-frontend-service/utils';
import { Toaster } from 'sonner';
import './index.css'; import './index.css';
const files = import.meta.glob('./app/**/*(page|layout).tsx'); const files = import.meta.glob('./app/**/*(page|layout).tsx');
@@ -34,6 +35,7 @@ if (!rootElement) throw new Error('Failed to find the root element');
createRoot(rootElement).render( createRoot(rootElement).render(
<StrictMode> <StrictMode>
<QueryProvider> <QueryProvider>
<Toaster />
<RouterProvider router={router} /> <RouterProvider router={router} />
</QueryProvider> </QueryProvider>
</StrictMode> </StrictMode>
-1
View File
@@ -1 +0,0 @@
/* You can add global styles to this file, and also import other style files */
+2 -2
View File
@@ -8,11 +8,11 @@ export default defineConfig(() => ({
root: __dirname, root: __dirname,
cacheDir: '../../node_modules/.vite/apps/gacha', cacheDir: '../../node_modules/.vite/apps/gacha',
server: { server: {
port: 4200, port: 3000,
host: 'localhost', host: 'localhost',
}, },
preview: { preview: {
port: 4300, port: 3001,
host: 'localhost', host: 'localhost',
}, },
plugins: [react(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], plugins: [react(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
+9
View File
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 351 KiB

+12
View File
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
+7
View File
@@ -0,0 +1,7 @@
# service
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test service` to execute the unit tests via [Vitest](https://vitest.dev/).
+12
View File
@@ -0,0 +1,12 @@
import nx from '@nx/eslint-plugin';
import baseConfig from '../../eslint.config.mjs';
export default [
...baseConfig,
...nx.configs['flat/react'],
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
},
];
+12
View File
@@ -0,0 +1,12 @@
{
"name": "@imphnen-frontend-service/service",
"version": "0.0.1",
"main": "./index.js",
"types": "./index.d.ts",
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js"
}
}
}
+23
View File
@@ -0,0 +1,23 @@
{
"name": "service",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/service/src",
"projectType": "library",
"tags": [],
"targets": {
"nx-release-publish": {
"options": {
"packageRoot": "dist/{projectRoot}"
}
}
},
"release": {
"version": {
"generatorOptions": {
"packageRoot": "dist/{projectRoot}",
"currentVersionResolver": "git-tag",
"fallbackCurrentVersionResolver": "disk"
}
}
}
}
+41
View File
@@ -0,0 +1,41 @@
import { api } from '../';
import {
TLoginRequest,
TLoginResponse,
TRegisterRequest,
TVerifyEmailRequest,
} from '../../types/auth';
import { TResponseMessage } from '../../types/common';
export const postLogin = async (
payload: TLoginRequest
): Promise<TLoginResponse> => {
const { data } = await api({
method: 'POST',
url: '/auth/login',
data: payload,
});
return data;
};
export const postRegister = async (
payload: TRegisterRequest
): Promise<TResponseMessage> => {
const { data } = await api({
method: 'POST',
url: '/auth/register',
data: payload,
});
return data;
};
export const postVerifyEmail = async (
payload: TVerifyEmailRequest
): Promise<TResponseMessage> => {
const { data } = await api({
method: 'POST',
url: '/auth/verify',
data: payload,
});
return data;
};
+1
View File
@@ -0,0 +1 @@
export {};
@@ -1,5 +1,9 @@
import axios, { AxiosRequestConfig } from 'axios'; import axios, { AxiosRequestConfig } from 'axios';
export * from './auth';
export * from './gacha';
export * from './users';
const config: AxiosRequestConfig = { const config: AxiosRequestConfig = {
baseURL: import.meta.env.VITE_API_URL, baseURL: import.meta.env.VITE_API_URL,
}; };
+1
View File
@@ -0,0 +1 @@
export {};
+52
View File
@@ -0,0 +1,52 @@
import { useMutation, UseMutationResult } from '@tanstack/react-query';
import { postLogin, postRegister, postVerifyEmail } from '../../api/auth';
import {
TLoginRequest,
TLoginResponse,
TRegisterRequest,
TVerifyEmailRequest,
} from '../../types/auth';
import { SessionToken, SessionUser } from '@imphnen-frontend-service/utils';
import { TResponseError, TResponseMessage } from '../../types/common';
export const usePostLogin = (): UseMutationResult<
TLoginResponse,
TResponseError,
TLoginRequest,
unknown
> => {
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();
},
});
};
export const usePostRegister = (): UseMutationResult<
TResponseMessage,
TResponseError,
TRegisterRequest,
unknown
> => {
return useMutation({
mutationKey: ['post-register'],
mutationFn: async (payload) => await postRegister(payload),
});
};
export const usePostVerifyEmail = (): UseMutationResult<
TResponseMessage,
TResponseError,
TVerifyEmailRequest,
unknown
> => {
return useMutation({
mutationKey: ['post-verify-email'],
mutationFn: async (payload) => await postVerifyEmail(payload),
});
};
+1
View File
@@ -0,0 +1 @@
export {};
+3
View File
@@ -0,0 +1,3 @@
export * from './auth';
export * from './gacha';
export * from './users';
+1
View File
@@ -0,0 +1 @@
export {};
+4
View File
@@ -0,0 +1,4 @@
export * from './api';
export * from './hooks';
export * from './types';
export * from './schemas';
+78
View File
@@ -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
);

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