Compare commits

...
Author SHA1 Message Date
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
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
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
boboiazumi a3887519f9 feat: Login Page 2025-03-28 11:46:27 +07:00
59 changed files with 1866 additions and 139 deletions
@@ -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...
-->
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

+1 -1
View File
@@ -7,7 +7,7 @@ export const AppLayout: FC = (): ReactElement => {
<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] mx-auto">
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
<Outlet />
</div>
</div>
+22 -13
View File
@@ -1,13 +1,13 @@
import * as React from 'react';
import { FC, ReactElement } from 'react';
import { FC, ReactElement, useState } from 'react';
import {
FilterOutlined,
SearchOutlined,
EditOutlined,
} from '@ant-design/icons';
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
import { DataTable } from '@imphnen-frontend-service/ui/organisms';
import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms';
import {
ColumnDef,
@@ -32,7 +32,7 @@ const mockData: Account[] = Array.from({ length: 90 }, (_, i) => ({
name: i === 0 ? 'Ahmad Wijuana' : 'Nama Lengkap',
email: 'fullname23@gmail.com',
phone: '081904423804',
address: 'Jl. Pantai Cibaduyut Indonesia',
address: 'Jl. Pantai Cibaduyut Indah',
}));
const columns: ColumnDef<Account>[] = [
@@ -100,6 +100,7 @@ export const Components: FC = (): ReactElement => {
});
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
const [showFilter, setShowFilter] = useState(false);
const table = useReactTable({
data: mockData,
@@ -120,7 +121,7 @@ export const Components: FC = (): ReactElement => {
return (
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
{/* Header */}
<header className="bg-white py-4 px-8 rounded-md shadow p-4">
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
<h1 className="text-p2 font-semibold">Data Akun</h1>
</header>
@@ -138,15 +139,23 @@ export const Components: FC = (): ReactElement => {
</div>
</div>
<Button
variant="primary"
disabled
size="md"
className="flex items-center gap-3"
>
<FilterOutlined />
Filters
</Button>
<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 */}
+1 -1
View File
@@ -7,7 +7,7 @@ export const AppLayout: FC = (): ReactElement => {
<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] mx-auto">
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
<Outlet />
</div>
</div>
+55 -21
View File
@@ -1,4 +1,10 @@
import { ArrowDownOutlined, PlusOutlined } from '@ant-design/icons';
import {
PlusOutlined,
ReloadOutlined,
UsergroupAddOutlined,
UsergroupDeleteOutlined,
UserSwitchOutlined,
} from '@ant-design/icons';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { FC, ReactElement } from 'react';
@@ -6,7 +12,7 @@ export const Components: FC = (): ReactElement => {
return (
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
{/* Dashboard Header */}
<header className="bg-white py-4 px-8 rounded-md shadow p-4">
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
<h1 className="text-p2 font-semibold">Dashboard</h1>
</header>
@@ -18,23 +24,51 @@ export const Components: FC = (): ReactElement => {
Summary
</h2>
<div className="grid grid-cols-2 gap-4">
{/* Summary Cards */}
{[1, 2, 3, 4].map((item) => (
<div
key={item}
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">
<ArrowDownOutlined 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">
Total Participants
</p>
</div>
{/* 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>
@@ -78,7 +112,7 @@ export const Components: FC = (): ReactElement => {
<Button
variant="text"
size="sm"
className="text-[10px] text-neutral-500 p-0 font-normal hover:bg-transparent hover:text-red-500"
className="text-[10px] text-red-500 p-0 font-normal hover:bg-transparent hover:text-red-700"
>
Delete
</Button>
@@ -87,7 +121,7 @@ export const Components: FC = (): ReactElement => {
{/* Lebih baik gunakan gambar yang sudah di-clip dengan size height: 78px daripada hard-code object-position dan margin */}
<img
src="gacha-clip.png"
src="gacha-clip.webp"
alt="Lanyard IMPHNEN"
className="h-full"
/>
@@ -99,7 +133,7 @@ export const Components: FC = (): ReactElement => {
{/* Right-side illustration */}
<img
src="gacha.png"
src="gacha.webp"
alt=""
className="rounded-lg hidden xl:block xl:min-w-[436px] h-auto object-cover"
/>
+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;
+232
View File
@@ -0,0 +1,232 @@
import * as React from 'react';
import { FC, 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';
type Status = 'valid' | 'invalid' | 'unchecked';
interface Prize {
id: number;
name: string;
orderValid: Status;
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 Status,
items: items[i % items.length],
address: 'Jl. Pantai Cibaduyut Indah',
status: (i % 3 === 0
? 'unchecked'
: i % 5 === 0
? 'invalid'
: 'valid') as Status,
}));
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<Status, 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<Status, 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> = {
valid: 'bg-success-200 text-success-500',
invalid: 'bg-danger-200 text-danger-500',
unchecked: 'bg-warning-200 text-warning-900',
};
const statusText: Record<Status, 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();
// handleUpdate(row.id);
}}
className="flex items-center gap-2 w-full"
>
<AuditOutlined className="text-[16px]" /> Process
</Button>
),
},
];
export const Components: FC = (): ReactElement => {
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 9,
});
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
const [showFilter, setShowFilter] = useState(false);
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 (
<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 onClose={() => setShowFilter(false)} />
</div>
)}
</div>
</div>
{/* Table */}
<DataTable data={mockData} columns={columns} table={table} />
</section>
</main>
);
};
export default Components;
@@ -7,7 +7,7 @@ export const AppLayout: FC = (): ReactElement => {
<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] mx-auto">
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
<Outlet />
</div>
</div>
+25 -17
View File
@@ -1,13 +1,12 @@
import * as React from 'react';
import { FC, ReactElement } from 'react';
import { FC, ReactElement, useState } from 'react';
import {
FilterOutlined,
SearchOutlined,
AuditOutlined,
} from '@ant-design/icons';
import { Button, Input } from '@imphnen-frontend-service/ui/atoms';
import { DataTable } from '@imphnen-frontend-service/ui/organisms';
import { DataTable, Filter } from '@imphnen-frontend-service/ui/organisms';
import {
ColumnDef,
@@ -39,7 +38,7 @@ const mockTransactions: Transaction[] = Array.from({ length: 20 }, (_, i) => ({
: 'valid') as TransactionStatus,
}));
const columns: ColumnDef<Account>[] = [
const columns: ColumnDef<Transaction>[] = [
{
id: 'select',
header: ({ table }) => (
@@ -77,9 +76,9 @@ const columns: ColumnDef<Account>[] = [
cell: ({ row }) => {
const status = row.original.status;
const statusColors: Record<TransactionStatus, string> = {
valid: 'bg-success-500 text-white',
invalid: 'bg-danger-500 text-white',
unchecked: 'bg-yellow-400 text-black',
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',
@@ -88,7 +87,7 @@ const columns: ColumnDef<Account>[] = [
};
return (
<div
className={`py-1 px-3 rounded-md text-center ${statusColors[status]}`}
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
>
{statusText[status]}
</div>
@@ -120,6 +119,7 @@ export const Components: FC = (): ReactElement => {
});
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
const [showFilter, setShowFilter] = useState(false);
const table = useReactTable({
data: mockTransactions,
@@ -140,7 +140,7 @@ export const Components: FC = (): ReactElement => {
return (
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
{/* Header */}
<header className="bg-white py-4 px-8 rounded-md shadow p-4">
<header className="bg-white py-4 px-8 rounded-lg shadow p-4">
<h1 className="text-p2 font-semibold">Validasi Transaksi</h1>
</header>
@@ -158,14 +158,22 @@ export const Components: FC = (): ReactElement => {
</div>
</div>
<Button
variant="primary"
size="md"
className="flex items-center gap-3"
>
<FilterOutlined />
Filters
</Button>
<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 onClose={() => setShowFilter(false)} />
</div>
)}
</div>
</div>
{/* Table */}
+7 -7
View File
@@ -17,14 +17,14 @@
--color-neutral-50: #f6f6f6;
--color-neutral-100: #e7e7e7;
--color-neutral-200: #d0d0d0;
--color-neutral-300: #b7b7b7;
--color-neutral-400: #9f9f9f;
--color-neutral-500: #7e7e7e;
--color-neutral-200: #d1d1d1;
--color-neutral-300: #b0b0b0;
--color-neutral-400: #888888;
--color-neutral-500: #6d6d6d;
--color-neutral-600: #5d5d5d;
--color-neutral-700: #4a4a4a;
--color-neutral-800: #3a3a3a;
--color-neutral-900: #2f2f2f;
--color-neutral-700: #4f4f4f;
--color-neutral-800: #454545;
--color-neutral-900: #3d3d3d;
--color-neutral-950: #2b2b2b;
--color-success-100: #e0fbd8;
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,45 @@
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 { InputForm } 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>
<InputForm label='Email' error={error ? error : undefined} size='lg' className='w-full' placeholder='Masukkan email-mu, Senpai~! ✨ (Pastikan tidak typo, ya~ 😆)' />
<InputForm 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;
@@ -0,0 +1,149 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import {
InputForm,
Modal,
Stepper,
} from '@imphnen-frontend-service/ui/molecules';
import { useQueryState } from '../../../hooks/use-query-state';
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) => (
<>
<InputForm
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) => (
<>
{/* TODO: Change component using OTP Input */}
<InputForm
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>
</>
);
interface IStepThreeProps {
onClose: () => void;
resetStep: () => void;
}
const StepThree = ({ onClose, resetStep }: IStepThreeProps) => (
<>
<InputForm
label="Password Baru"
placeholder="Masukkan Password Baru"
type="password"
size="lg"
className="w-full"
/>
<InputForm
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,65 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputForm, Modal } from '@imphnen-frontend-service/ui/molecules';
import { Link } from 'react-router-dom';
interface IModalFormLogin {
isOpen: boolean;
onClose: () => void;
onForgotPassword: () => void;
}
const ModalFormLogin = ({
isOpen,
onClose,
onForgotPassword,
}: IModalFormLogin) => {
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 className="space-y-4">
<InputForm
label="Email"
placeholder="Masukkan Email"
type="email"
size="lg"
className="w-full"
/>
<InputForm
label="Password"
placeholder="Masukkan Password"
type="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 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>
<Link
className="text-primary-500 hover:text-primary-600"
to="/register"
>
Daftar
</Link>
</div>
</Modal.Content>
</Modal>
);
};
export default ModalFormLogin;
@@ -0,0 +1,123 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputForm, Modal } from '@imphnen-frontend-service/ui/molecules';
import { useQueryState } from '../../../hooks/use-query-state';
interface IModalFormRegisterProps {
isOpen: boolean;
onClose: () => void;
}
const ModalFormRegister = ({ isOpen, onClose }: IModalFormRegisterProps) => {
const {
step: currentStep,
nextStep,
prevStep,
resetStep,
} = useQueryState('registerStep', {
defaultValue: 1,
maxValue: 2,
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">
Register
</h1>
<h2 className="text-neutral-500 text-lg text-center">
{currentStep === 1 ? 'Info Akun' : 'Informasi Pengiriman Hadiah'}
</h2>
</Modal.Header>
<Modal.Content className="space-y-6">
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
{currentStep === 2 && (
<StepTwo nextStep={nextStep} prevStep={prevStep} />
)}
</Modal.Content>
</Modal>
);
};
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
}
const StepOne = ({ nextStep, onClose }: IStepOneProps) => (
<>
<InputForm
label="Nama Lengkap"
placeholder="Masukkan Nama Lengkap"
type="text"
size="lg"
className="w-full"
/>
<InputForm
label="Email"
placeholder="Masukkan Email Anda"
type="email"
size="lg"
className="w-full"
/>
<InputForm
label="Password"
placeholder="Masukkan Password"
type="password"
size="lg"
className="w-full"
/>
<InputForm
label="Ulangi Password"
placeholder="Masukkan Ulang Password"
type="password"
size="lg"
className="w-full"
/>
<Button size="md" className="w-full" onClick={nextStep}>
Selanjutnya
</Button>
</>
);
interface IStepTwoProps {
nextStep: () => void;
prevStep: () => void;
}
const StepTwo = ({ nextStep, prevStep }: IStepTwoProps) => (
<>
<InputForm
label="Nomor Telepon"
placeholder="Masukkan Nomor Telepon Aktif"
type="text"
size="lg"
className="w-full"
/>
<InputForm
label="Alamat Pengiriman"
placeholder="Masukkan Alamat Pengiriman"
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" onClick={nextStep}>
Gacha Sekarang
</Button>
</div>
</>
);
export default ModalFormRegister;
+49 -58
View File
@@ -1,9 +1,10 @@
import { ArrowDownOutlined } from '@ant-design/icons';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputForm, Modal } from '@imphnen-frontend-service/ui/molecules';
import { cn } from '@imphnen-frontend-service/utils';
import { FC, Fragment, ReactElement, useEffect, useState } from 'react';
import { Link } from 'react-router';
import ModalFormForgotPassword from './_components/form/modal-form-forgot-password';
import ModalFormLogin from './_components/form/modal-form-login';
import ModalFormRegister from './_components/form/modal-form-register';
interface GachaItemProps {
src: string;
@@ -12,8 +13,9 @@ interface GachaItemProps {
}
export const Components: FC = (): ReactElement => {
// Pinjem
const [showModal, setShowModal] = useState(false);
const [showModalForgotPassword, setShowModalForgotPassword] = useState(false);
const [showModalLogin, setShowModalLogin] = useState(false);
const [showModalRegister, setShowModalRegister] = useState(false);
const scrollToRoulette = () => {
const rouletteSection = document.getElementById('roulette');
@@ -65,6 +67,11 @@ export const Components: FC = (): ReactElement => {
);
};
const handleForgotPasswordClick = () => {
setShowModalLogin(false);
setShowModalForgotPassword(true);
};
return (
<Fragment>
{/* Landing Page */}
@@ -216,64 +223,48 @@ export const Components: FC = (): ReactElement => {
Spin Now
</Button>
</div>
{/* TODO: Change using real button trigger */}
<div className="flex gap-4 col-span-4 md:col-span-8 lg:col-span-6 justify-center items-center">
<Button onClick={() => setShowModalLogin(true)}>
Open Modal Login
</Button>
<Button onClick={() => setShowModalForgotPassword(true)}>
Open Modal Forgot Password
</Button>
<Button onClick={() => setShowModalRegister(true)}>
Open Modal Register
</Button>
</div>
</section>
{/* Diffuser & Cloud */}
<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>
{/**Izin pake untuk debug modals gacha */}
<button onClick={() => setShowModal(true)}>Open Modal</button>
<Modal
className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7"
isOpen={showModal}
onClose={() => setShowModal(false)}
>
<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 className="space-y-4">
<InputForm
label="Email"
placeholder="Masukkan Email"
type="email"
size="lg"
className="w-full"
/>
<InputForm
label="Password"
placeholder="Masukkan password"
type="password"
size="lg"
className="w-full"
/>
<Link to="/forgot-password">
<h1 className="text-end text-primary-500 text-xl font-medium">
Lupa Password?
</h1>
</Link>
</Modal.Content>
<Modal.Footer className="flex flex-col md:flex-col pt-4">
<Button
size="md"
className="w-full"
onClick={() => console.log('Login')}
>
Login
</Button>
<div className="flex justify-center gap-2 pt-2.5 font-medium">
<p className="text-neutral-500">Belum punya akun?</p>
<Link
className="text-primary-500 hover:text-primary-600"
to="/register"
>
Daftar disini
</Link>
</div>
</Modal.Footer>
</Modal>
{/**Izin pake untuk debug modals gacha */}
{/* Login Modal */}
<ModalFormLogin
isOpen={showModalLogin}
onClose={() => setShowModalLogin(false)}
onForgotPassword={handleForgotPasswordClick}
key="login"
/>
{/* Register Modal */}
<ModalFormRegister
isOpen={showModalRegister}
onClose={() => {
setShowModalRegister(false);
}}
key="register"
/>
{/* Forgot Password Modal */}
<ModalFormForgotPassword
isOpen={showModalForgotPassword}
onClose={() => {
setShowModalForgotPassword(false);
setShowModalLogin(true);
}}
key="forgot-password"
/>
</Fragment>
);
};
+71
View File
@@ -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,
};
};
+7 -7
View File
@@ -17,14 +17,14 @@
--color-neutral-50: #f6f6f6;
--color-neutral-100: #e7e7e7;
--color-neutral-200: #d0d0d0;
--color-neutral-300: #b7b7b7;
--color-neutral-400: #9f9f9f;
--color-neutral-500: #7e7e7e;
--color-neutral-200: #d1d1d1;
--color-neutral-300: #b0b0b0;
--color-neutral-400: #888888;
--color-neutral-500: #6d6d6d;
--color-neutral-600: #5d5d5d;
--color-neutral-700: #4a4a4a;
--color-neutral-800: #3a3a3a;
--color-neutral-900: #2f2f2f;
--color-neutral-700: #4f4f4f;
--color-neutral-800: #454545;
--color-neutral-900: #3d3d3d;
--color-neutral-950: #2b2b2b;
--color-success-100: #e0fbd8;
+4
View File
@@ -11,6 +11,10 @@ const config: StorybookConfig = {
},
},
},
staticDirs: [
'../../../apps/backoffice/public',
'../../../apps/gacha/public',
'../../../apps/dimentorin/public', ],
};
export default config;
+1 -1
View File
@@ -51,7 +51,7 @@ export const Button: FC<TButtonProps> = ({
...rest
}): ReactElement => {
const mergedClassName = cn(
'inline-flex items-center justify-center font-[600] rounded-lg px-[16px] py-[10px]',
'inline-flex items-center justify-center font-[600] rounded-md px-[16px] py-[10px]',
'transition-colors duration-200 cursor-pointer',
sizeClasses[size],
variantClasses[variant],
+1 -1
View File
@@ -45,7 +45,7 @@ export const Input: FC<TInputProps> = ({
};
const mergedClassName = cn(
'px-[12px] py-[8px] text-neutral-800 placeholder:text-neutral-300 border border-neutral-200 hover:border-blue-300 focus:outline-1 focus:outline-blue-500 rounded-md font-bai-jamjuree min-w-70',
'px-[12px] py-[8px] text-neutral-800 bg-white placeholder:text-neutral-300 border border-neutral-200 hover:border-blue-300 focus:outline-1 focus:outline-blue-500 rounded-md font-bai-jamjuree min-w-70',
sizeClasses[size].textSize,
disabled && disabledClass,
className
+7 -7
View File
@@ -17,14 +17,14 @@
--color-neutral-50: #f6f6f6;
--color-neutral-100: #e7e7e7;
--color-neutral-200: #d0d0d0;
--color-neutral-300: #b7b7b7;
--color-neutral-400: #9f9f9f;
--color-neutral-500: #7e7e7e;
--color-neutral-200: #d1d1d1;
--color-neutral-300: #b0b0b0;
--color-neutral-400: #888888;
--color-neutral-500: #6d6d6d;
--color-neutral-600: #5d5d5d;
--color-neutral-700: #4a4a4a;
--color-neutral-800: #3a3a3a;
--color-neutral-900: #2f2f2f;
--color-neutral-700: #4f4f4f;
--color-neutral-800: #454545;
--color-neutral-900: #3d3d3d;
--color-neutral-950: #2b2b2b;
--color-success-100: #e0fbd8;
@@ -0,0 +1,30 @@
import { DetailedHTMLProps, FC, InputHTMLAttributes, ReactElement } from "react"
type TForgotStepProps = Omit<
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
'size' | 'type'
> & {
step: number
};
export const ForgotStep: FC<TForgotStepProps> = ({
step = 1,
...rest
}): ReactElement => {
return (
<div className="w-full grid grid-cols-3 gap-3">
<div className="flex flex-col items-center justify-center gap-2">
<div className={`${step === 1 ? "bg-primary-500" : "bg-primary-200"} px-5 py-2 rounded-md w-full`}></div>
<h4 className={`${step === 1 ? "text-primary-500" : "text-gray-500"} text-xs`}>Masukkan Email</h4>
</div>
<div className="flex flex-col items-center justify-center gap-2">
<div className={`${step === 2 ? "bg-primary-500" : "bg-primary-200"} px-5 py-2 rounded-md w-full`}></div>
<h4 className={`${step === 2 ? "text-primary-500" : "text-gray-500"} text-xs`}>Verifikasi OTP</h4>
</div>
<div className="flex flex-col items-center justify-center gap-2">
<div className={`${step === 3 ? "bg-primary-500" : "bg-primary-200"} px-5 py-2 rounded-md w-full`}></div>
<h4 className={`${step === 3 ? "text-primary-500" : "text-gray-500"} text-xs`}>Summon Password ^^</h4>
</div>
</div>
)
}
@@ -0,0 +1 @@
export * from './forgot-step';
+3
View File
@@ -1,3 +1,6 @@
export * from './forgot-step';
export * from './otp-form';
export * from './input-form';
export * from './pagination';
export * from './modal/modal';
export * from './stepper';
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react';
import { InputForm } from './input-form';
const meta = {
title: 'Molecules/InputForm',
title: 'Molecules/Input Form',
component: InputForm,
parameters: {
layout: 'centered',
+1
View File
@@ -0,0 +1 @@
export * from "./otp-form"
@@ -0,0 +1,61 @@
// eslint-disable-next-line @nx/enforce-module-boundaries
import { Input } from "@imphnen-frontend-service/ui/atoms";
import { ChangeEvent, DetailedHTMLProps, FC, InputHTMLAttributes, ReactElement, useRef, useState } from "react"
type TForgotStepProps = Omit<
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
'size' | 'type'
> & {
};
export const OtpForm: FC<TForgotStepProps> = ({
step = 1,
...rest
}): ReactElement => {
const [cell, setCell] = useState(new Array(6).fill(""))
const inputRef = useRef([])
const handleChange = (index: number, e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
if (!(/^[0-9]?$/.test(value))) return;
const newCell = [...cell]
newCell[index] = value
setCell(newCell)
if(value && index < 6 - 1){
(inputRef.current[index + 1] as HTMLInputElement).focus()
}
}
const handleKeyDown = (index: number, e: React.KeyboardEvent<HTMLInputElement>) => {
if(e.key === "Backspace" && !cell[index] && index > 0){
const newCell = [...cell]
newCell[index] = ""
setCell(newCell)
const ref = (inputRef.current[index - 1] as HTMLInputElement)
ref.focus()
}
}
return (
<div className="w-full grid grid-cols-6 gap-3 my-10">
{cell.map((v, i) => (
<Input
placeholder=""
ref={(el) => {
inputRef.current.push(el as never)
return inputRef.current[i]
}}
onChange={(e) => handleChange(i, e)}
onKeyDown={(e) => handleKeyDown(i, e)}
size="lg"
value={cell[i]}
className="sm:min-w-[10px]"
/>
))}
</div>
)
}
+1
View File
@@ -0,0 +1 @@
export * from './stepper';
@@ -0,0 +1,48 @@
import { render, screen } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { Stepper, StepperProps } from './Stepper';
vi.mock('@imphnen-frontend-service/utils', () => ({
cn: (...args: string[]) => args.filter(Boolean).join(' '),
}));
describe('Stepper', () => {
const renderStepper = (props: StepperProps) => render(<Stepper {...props} />);
it('renders with correct step information', () => {
renderStepper({ currentStep: 2, totalSteps: 5 });
expect(screen.getByText('Langkah 2 dari 5')).toBeInTheDocument();
});
it('applies custom className correctly', () => {
const customClass = 'custom-stepper';
renderStepper({
currentStep: 1,
totalSteps: 3,
className: customClass,
});
const stepperElement = screen.getByText('Langkah 1 dari 3').parentElement;
expect(stepperElement).toHaveClass('text-center');
expect(stepperElement).toHaveClass('mb-4');
expect(stepperElement).toHaveClass(customClass);
});
it('handles step 0 correctly', () => {
renderStepper({ currentStep: 0, totalSteps: 3 });
expect(screen.getByText('Langkah 0 dari 3')).toBeInTheDocument();
});
it('handles equal currentStep and totalSteps', () => {
renderStepper({ currentStep: 3, totalSteps: 3 });
expect(screen.getByText('Langkah 3 dari 3')).toBeInTheDocument();
});
it('applies correct text styling classes', () => {
renderStepper({ currentStep: 1, totalSteps: 2 });
const textElement = screen.getByText('Langkah 1 dari 2');
expect(textElement).toHaveClass('text-neutral-400');
expect(textElement).toHaveClass('text-lg');
});
});
@@ -0,0 +1,61 @@
import { Meta, StoryObj } from '@storybook/react';
import { Stepper } from './Stepper';
const meta: Meta<typeof Stepper> = {
title: 'Components/Stepper',
component: Stepper,
argTypes: {
currentStep: {
control: { type: 'number', min: 0 },
description: 'Current step number',
},
totalSteps: {
control: { type: 'number', min: 1 },
description: 'Total number of steps',
},
className: {
control: 'text',
description: 'Custom CSS class',
},
},
parameters: {
docs: {
description: {
component: 'A simple stepper component showing progress through steps',
},
},
},
};
export default meta;
type Story = StoryObj<typeof Stepper>;
export const Default: Story = {
args: {
currentStep: 1,
totalSteps: 3,
},
};
export const MiddleStep: Story = {
args: {
currentStep: 2,
totalSteps: 4,
},
};
export const LastStep: Story = {
args: {
currentStep: 3,
totalSteps: 3,
},
};
export const WithCustomClass: Story = {
args: {
currentStep: 1,
totalSteps: 5,
className: 'custom-class',
},
};
+25
View File
@@ -0,0 +1,25 @@
import { cn } from '@imphnen-frontend-service/utils';
import React from 'react';
interface StepperProps {
currentStep: number;
totalSteps: number;
className?: string;
}
const Stepper: React.FC<StepperProps> = ({
currentStep,
totalSteps,
className,
}) => {
return (
<div className={cn('text-center mb-4', className)}>
<p className="text-neutral-400 text-lg">
Langkah {currentStep} dari {totalSteps}
</p>
</div>
);
};
export { Stepper };
export type { StepperProps };
@@ -0,0 +1,29 @@
import { ArrowLeftOutlined } from "@ant-design/icons";
// eslint-disable-next-line @nx/enforce-module-boundaries
import { Button } from "@imphnen-frontend-service/ui/atoms";
import { FC, ReactElement } from "react";
function AuthBanner({text, href}: {text: string, href: string}){
return (
<div className='relative rounded-md'>
<img src="/image/95319c4f9953dfe6180200e529dfcea5.webp" alt="Banner" className='w-[420px] h-[632px] object-[80%] object-cover rounded-lg' />
<div className='absolute top-0 bg-gradient-to-b from-primary-500 to-transparent w-full rounded-t-lg h-[163px] py-5 px-5'>
<Button variant='secondary' className='gap-2' onClick={() => document.location.href = `${href}`}>
<ArrowLeftOutlined />
<p>{text}</p>
</Button>
</div>
<div className='absolute bottom-0 bg-gradient-to-t from-primary-500 to-transparent w-full rounded-b-lg h-[263px] flex flex-col items-center justify-center'>
<img src="/image/9261045e09137f3fcb925a78c55b6ddb.webp" alt="Logo" width={317} />
</div>
</div>
)
}
export const LoginBanner: FC = (): ReactElement => {
return AuthBanner({text: "Back To Homepage", href: "/"})
}
export const RegisterResetBanner: FC = (): ReactElement => {
return AuthBanner({text: "Back To Login", href: "/auth/login"})
}
@@ -0,0 +1 @@
export * from './auth-banner';
@@ -1,6 +1,7 @@
import {
AppstoreOutlined,
AuditOutlined,
InboxOutlined,
LogoutOutlined,
UserOutlined,
} from '@ant-design/icons';
@@ -56,6 +57,18 @@ export const BackofficeSidebar: FC = (): ReactElement => {
<AuditOutlined className="text-[20px]" />
<span className="text-p3 font-medium">Validasi Transaksi</span>
</Link>
<Link
to="/prizes"
className={`flex items-center justify-items-start gap-3 px-[8px] py-[10px] ${
isActive('/prizes')
? 'bg-primary-500 text-white rounded-md'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
<InboxOutlined className="text-[20px]" />
<span className="text-p3 font-medium">Data Pengiriman Hadiah</span>
</Link>
</nav>
</div>
@@ -5,6 +5,7 @@ import {
getPaginationRowModel,
flexRender,
ColumnDef,
Table,
} from '@tanstack/react-table';
import { Pagination } from '../../molecules';
@@ -13,6 +14,7 @@ import React from 'react';
interface DataTableProps<T> {
data: T[];
columns: ColumnDef<T>[];
table: Table<T>;
pageSize?: number;
}
@@ -47,7 +49,7 @@ export const DataTable = <T,>({
{headerGroup.headers.map((header) => (
<th
key={header.id}
className="py-3 px-5 font-normal first:rounded-l-lg last:rounded-r-lg"
className="py-4 px-5 font-normal first:rounded-l-lg last:rounded-r-lg"
>
{header.isPlaceholder
? null
@@ -60,7 +62,7 @@ export const DataTable = <T,>({
</tr>
))}
</thead>
<tbody className="mt-3">
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id} className="bg-primary-100 odd:bg-white">
{row.getVisibleCells().map((cell, index) => (
@@ -0,0 +1,21 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Filter } from './filter';
const meta: Meta<typeof Filter> = {
title: 'Organisms/Filter',
component: Filter,
tags: ['autodocs'],
parameters: {
backgrounds: {
default: 'neutral',
values: [{ name: 'neutral', value: '#e7e7e7' }],
},
},
} satisfies Meta<typeof Filter>;
export default meta;
type Story = StoryObj<typeof Filter>;
export const Default: Story = {
args: {},
};
+89
View File
@@ -0,0 +1,89 @@
import { CloseOutlined } from '@ant-design/icons';
import { useState } from 'react';
interface RadioProps {
checked?: boolean;
disabled?: boolean;
id: string;
label?: string;
name: string;
value: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
const Radio = ({
checked,
disabled,
id,
label,
name,
value,
onChange,
}: RadioProps) => (
<div className="flex gap-2 items-center">
<div className="relative grid place-items-center mt-1">
<input
type="radio"
id={id}
name={name}
value={value}
checked={checked}
onChange={onChange}
disabled={disabled}
className="peer col-start-1 row-start-1 appearance-none shrink-0 size-[10px] bg-primary-100 rounded-full disabled:border-gray-400"
/>
<div className="pointer-events-none col-start-1 row-start-1 size-[6px] rounded-full peer-checked:bg-primary-500 peer-checked:peer-disabled:bg-gray-400" />
</div>
<label htmlFor={id} className="text-start text-neutral-400 text-label2">
{label || 'This is the radio label'}
</label>
</div>
);
interface FilterProps {
onClose?: () => void;
}
export const Filter = ({ onClose }: FilterProps) => {
const [selectedStatus, setSelectedStatus] = useState('delivered');
const handleStatusChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSelectedStatus(e.target.value);
};
return (
<div className="inline-flex flex-col p-[20px] bg-white rounded-lg gap-4 w-[122px] shadow">
<div className="flex justify-between items-baseline">
<span className="font-semibold text-p3 text-primary-500">Filters</span>
<button
onClick={onClose}
className="cursor-pointer text-neutral-400 hover:text-neutral-600"
>
<CloseOutlined className="text-[12px]" />
</button>
</div>
<hr className="border-primary-200" />
<span className="font-semibold text-primary-500">Status</span>
<div className="flex flex-col gap-[10px]">
<Radio
id="option1"
name="status"
value="undelivered"
label="Undelivered"
checked={selectedStatus === 'undelivered'}
onChange={handleStatusChange}
/>
<Radio
id="option2"
name="status"
value="delivered"
label="Delivered"
checked={selectedStatus === 'delivered'}
onChange={handleStatusChange}
/>
</div>
</div>
);
};
export default Filter;
+1
View File
@@ -0,0 +1 @@
export * from './filter';
+3 -1
View File
@@ -1,4 +1,6 @@
export * from './navbar';
export * from './modals-gacha';
export * from "./modals-gacha";
export * from "./auth-banner";
export * from './backoffice-sidebar'
export * from './datatable'
export * from './filter'
@@ -0,0 +1 @@
export * from './modal-backoffice';
@@ -0,0 +1,25 @@
import { ReactElement, ReactNode } from 'react';
interface ModalBackofficeProps {
children: ReactNode;
className?: string;
}
export const ModalBackoffice = ({
children,
className,
...rest
}: ModalBackofficeProps): ReactElement => {
return (
<div
className={`w-[400px] bg-primary-50 rounded-lg p-[40px] gap-[32px] ${
className || ''
}`}
{...rest}
>
{children}
</div>
);
};
export default ModalBackoffice;
@@ -0,0 +1,373 @@
import type { Meta, StoryObj } from '@storybook/react';
import { ModalBackoffice } from './modal-backoffice';
import { InputForm } from '../../molecules';
import { Button } from '../../atoms';
const meta: Meta<typeof ModalBackoffice> = {
title: 'Organisms/Modal Backoffice',
component: ModalBackoffice,
tags: ['autodocs'],
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof ModalBackoffice>;
export default meta;
type Story = StoryObj<typeof ModalBackoffice>;
export const Default: Story = {
args: {
// isOpen: true,
// onClose: () => console.log('Modal closed'),
// title: 'Sample Modal',
children: (
<>
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Modal Title
</h2>
<p className="text-p3 text-neutral-400">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quas repellat
rerum doloremque dolorem fugit architecto eos blanditiis ratione
facere? Esse dolores inventore deleniti. Dicta voluptatem dolore vel
quia amet dolorem?
</p>
<div className="flex flex-col gap-4"></div>
</>
),
},
};
export const TambahItemGacha: Story = {
args: {
children: (
<div className="flex flex-col gap-8">
<div>
<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>
</div>
<div className="flex flex-col gap-4">
<InputForm
label="Nama Hadiah"
type="text"
placeholder="Masukkan Nama Hadiah"
size="lg"
className="w-full"
/>
<InputForm
label="Chance Rate"
type="text"
placeholder="Masukkan Chance Rate"
size="lg"
className="w-full"
/>
<InputForm
label="Foto Barang"
type="text"
placeholder=".jpg, .jpeg, atau .png"
size="lg"
className="w-full"
/>
</div>
<Button variant="primary" size="lg" className="w-full">
Tambahkan Item
</Button>
</div>
),
},
};
export const TambahItem: Story = {
args: {
children: (
<div className="flex flex-col gap-8 text-center">
<div>
<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>
</div>
<div className="flex gap-4">
<Button variant="bordered" size="lg" className="w-full">
Batal
</Button>
<Button variant="primary" size="lg" className="w-full">
Tambahkan
</Button>
</div>
</div>
),
},
};
export const EditItemGacha: Story = {
args: {
children: (
<div className="flex flex-col gap-8">
<div>
<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>
</div>
<div className="flex flex-col gap-4">
<InputForm
label="Nama Hadiah"
type="text"
placeholder="Masukkan Nama Hadiah"
size="lg"
className="w-full"
/>
<InputForm
label="Chance Rate"
type="text"
placeholder="Masukkan Chance Rate"
size="lg"
className="w-full"
/>
<InputForm
label="Foto Barang"
type="text"
placeholder=".jpg, .jpeg, atau .png"
size="lg"
className="w-full"
/>
</div>
<Button variant="primary" size="lg" className="w-full">
Perbarui Item
</Button>
</div>
),
},
};
export const UpdateItem: Story = {
args: {
children: (
<div className="flex flex-col gap-8 text-center">
<div>
<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>
</div>
<div className="flex gap-4">
<Button variant="bordered" size="lg" className="w-full">
Batal
</Button>
<Button variant="primary" size="lg" className="w-full">
Update
</Button>
</div>
</div>
),
},
};
export const DeleteItem: Story = {
args: {
children: (
<div className="flex flex-col gap-8 text-center">
<img
src="/chibi-delete.webp"
alt="Delete item?"
width={148}
className="self-center"
/>
<div>
<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>
<div className="flex gap-4">
<Button variant="secondary" size="lg" className="w-full">
Batal Hapus
</Button>
<Button variant="danger" size="lg" className="w-full">
Hapus Item
</Button>
</div>
</div>
),
},
};
export const EditDataAkun: Story = {
args: {
children: (
<div className="flex flex-col gap-8">
<h2 className="text-p1 font-semibold text-primary-500 mb-3">
Edit Data Akun
</h2>
<div className="flex flex-col gap-4">
<InputForm
label="Nama Lengkap"
type="text"
placeholder="Masukkan Nama Lengkap"
value="Ahmad Wiyana"
size="lg"
className="w-full"
/>
<InputForm
label="Email"
type="text"
placeholder="Masukkan Email"
value="fullname23@gmail.com"
size="lg"
className="w-full"
/>
<InputForm
label="Nomor Telepon"
type="text"
placeholder="Masukkan Nomor Telepon"
value="081904423804"
size="lg"
className="w-full"
/>
<InputForm
label="Alamat"
type="text"
placeholder="Masukkan Alamat"
value="Jl. Pantai Cibaduyut Indah"
size="lg"
className="w-full"
/>
</div>
<Button variant="primary" size="lg" className="w-full">
Perbarui Data
</Button>
</div>
),
},
};
export const UpdateDataAkun: Story = {
args: {
children: (
<div className="flex flex-col gap-8 text-center">
<div>
<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>
</div>
<div className="flex gap-4">
<Button variant="bordered" size="lg" className="w-full">
Batal
</Button>
<Button variant="primary" size="lg" className="w-full">
Update
</Button>
</div>
</div>
),
},
};
export const ValidasiTransaksi: Story = {
args: {
children: (
<div className="flex flex-col gap-8">
<h2 className="text-p1 font-semibold text-primary-500 ">
Validasi Transaksi
</h2>
<InputForm
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"
>
Tidak Valid
</Button>
<Button variant="primary" size="lg" className="w-full">
Valid
</Button>
</div>
</div>
),
},
};
export const ProsesDelivery: Story = {
args: {
children: (
<div className="flex flex-col gap-8">
<div>
<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>
</div>
<div className="flex flex-col gap-4">
<InputForm
label="Nama Lengkap"
type="text"
placeholder="Masukkan Nama Lengkap"
value="Ahmad Wiyana"
size="lg"
className="w-full"
/>
<InputForm
label="Item yang didapatkan"
type="text"
placeholder="Masukkan Nama Item"
value="Lanyard + ID Card"
size="lg"
className="w-full"
/>
<InputForm
label="Alamat Pengiriman"
type="text"
placeholder="Masukkan Alamat Pengiriman"
value="Jl. Pantai Cibaduyut Indah"
size="lg"
className="w-full"
/>
<InputForm
label="Status"
type="text"
placeholder="Isi Status Pengiriman"
value="Lanyard + ID Card"
size="lg"
className="w-full"
/>
</div>
<Button variant="primary" size="lg" className="w-full">
Tambahkan Item
</Button>
</div>
),
},
};