Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47ebd3ad73 | ||
|
|
d544e43abc | ||
|
|
809eeee931 | ||
|
|
f39466936f | ||
|
|
5d418830b9 | ||
|
|
d58ad58ef8 | ||
|
|
b62c762cf9 | ||
|
|
4e665cca76 | ||
|
|
74c0ebc5dd | ||
|
|
eadd554a32 | ||
|
|
ecdaa4a82e | ||
|
|
747e75e684 | ||
|
|
76637dd051 | ||
|
|
3dda8149ba | ||
|
|
cc21735468 | ||
|
|
a6bc6de72e | ||
|
|
340bd8c4bf | ||
|
|
1594be9d46 | ||
|
|
7eecf2621d | ||
|
|
088a0fc41f | ||
|
|
9178c65c4a | ||
|
|
f93d741cef | ||
|
|
81fa73935f | ||
|
|
88c71ccdfa | ||
|
|
5b3b9e44fa | ||
|
|
ac2ff2ec92 | ||
|
|
79e0d39280 | ||
|
|
fd9113109c | ||
|
|
da7e1067d2 | ||
|
|
75779b0373 | ||
|
|
a3887519f9 | ||
|
|
2ec6ad5b5c | ||
|
|
2d541c71ba | ||
|
|
46f3bd6014 | ||
|
|
c374c97e66 | ||
|
|
a4ca53004f | ||
|
|
2d9473ea7c | ||
|
|
6bfa48bed8 | ||
|
|
43167fe7a1 | ||
|
|
39391e5522 | ||
|
|
ec509a7b43 | ||
|
|
a76bd89ec1 | ||
|
|
cafdb29024 | ||
|
|
d79d0d3d09 | ||
|
|
7606c5b4e2 | ||
|
|
bc9a0fd740 | ||
|
|
7f9a106fca | ||
|
|
0bcc8f7f6a | ||
|
|
31b7587571 | ||
|
|
82fb1ea2bc | ||
|
|
bd5e46e712 | ||
|
|
542469bc67 | ||
|
|
631684cb05 | ||
|
|
54f4572533 | ||
|
|
ca933f624a | ||
|
|
90ab974e03 | ||
|
|
daeeb09798 | ||
|
|
c8935b885e |
@@ -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...
|
||||||
|
-->
|
||||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 351 KiB |
|
After Width: | Height: | Size: 511 KiB |
@@ -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,168 @@
|
|||||||
|
import * as React 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, Filter } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ColumnDef,
|
||||||
|
getCoreRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
PaginationState,
|
||||||
|
useReactTable,
|
||||||
|
RowSelectionState,
|
||||||
|
} from '@tanstack/react-table';
|
||||||
|
|
||||||
|
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',
|
||||||
|
}));
|
||||||
|
|
||||||
|
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();
|
||||||
|
// handleEdit(row.id);
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<EditOutlined /> Edit
|
||||||
|
</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 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -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,145 @@
|
|||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
UsergroupAddOutlined,
|
||||||
|
UsergroupDeleteOutlined,
|
||||||
|
UserSwitchOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { FC, ReactElement } from 'react';
|
||||||
|
|
||||||
|
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-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">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
size="sm"
|
||||||
|
className="text-[10px] text-red-500 p-0 font-normal hover:bg-transparent hover:text-red-700"
|
||||||
|
>
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import { FC, ReactElement } from 'react';
|
import { FC, ReactElement } from 'react';
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
import { Navbar } from '@imphnen-frontend-service/ui/organisms';
|
|
||||||
|
|
||||||
export const AppLayout: FC = (): ReactElement => {
|
export const AppLayout: FC = (): ReactElement => {
|
||||||
return (
|
return (
|
||||||
<main className="bg-primary-50 min-h-screen">
|
<main className="bg-primary-50 min-h-screen">
|
||||||
<Navbar />
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,36 @@
|
|||||||
import { FC, ReactElement } from 'react';
|
import { FC, ReactElement } from 'react';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { InputForm } from '@imphnen-frontend-service/ui/molecules';
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
export const Components: FC = (): ReactElement => {
|
||||||
return <>Hallo</>;
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
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>
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<Button onClick={() => navigate('/dashboard')}>Login</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Components;
|
export default Components;
|
||||||
|
|||||||
@@ -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,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;
|
||||||
@@ -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,186 @@
|
|||||||
|
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 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,
|
||||||
|
}));
|
||||||
|
|
||||||
|
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();
|
||||||
|
// handleUpdate(row.id);
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2 w-full"
|
||||||
|
>
|
||||||
|
<AuditOutlined className="text-[16px]" /> Update
|
||||||
|
</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: 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 (
|
||||||
|
<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 onClose={() => setShowFilter(false)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<DataTable data={mockTransactions} columns={columns} table={table} />
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -17,59 +17,55 @@
|
|||||||
|
|
||||||
--color-neutral-50: #f6f6f6;
|
--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-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;
|
||||||
@@ -137,4 +133,4 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
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;
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { ArrowDownOutlined } from '@ant-design/icons';
|
import { ArrowDownOutlined } from '@ant-design/icons';
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import { ModalsGacha } from '@imphnen-frontend-service/ui/organisms';
|
|
||||||
import { cn } from '@imphnen-frontend-service/utils';
|
import { cn } from '@imphnen-frontend-service/utils';
|
||||||
import { FC, Fragment, ReactElement, useEffect, useState } from 'react';
|
import { FC, Fragment, ReactElement, useEffect, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
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 {
|
interface GachaItemProps {
|
||||||
src: string;
|
src: string;
|
||||||
@@ -12,9 +13,9 @@ interface GachaItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
export const Components: FC = (): ReactElement => {
|
||||||
// Pinjem
|
const [showModalForgotPassword, setShowModalForgotPassword] = useState(false);
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModalLogin, setShowModalLogin] = useState(false);
|
||||||
//
|
const [showModalRegister, setShowModalRegister] = useState(false);
|
||||||
|
|
||||||
const scrollToRoulette = () => {
|
const scrollToRoulette = () => {
|
||||||
const rouletteSection = document.getElementById('roulette');
|
const rouletteSection = document.getElementById('roulette');
|
||||||
@@ -66,6 +67,11 @@ export const Components: FC = (): ReactElement => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleForgotPasswordClick = () => {
|
||||||
|
setShowModalLogin(false);
|
||||||
|
setShowModalForgotPassword(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{/* Landing Page */}
|
{/* Landing Page */}
|
||||||
@@ -217,22 +223,48 @@ export const Components: FC = (): ReactElement => {
|
|||||||
Spin Now
|
Spin Now
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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>
|
</section>
|
||||||
{/* Diffuser & Cloud */}
|
{/* 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>
|
<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 */}
|
{/* Login Modal */}
|
||||||
<div>
|
<ModalFormLogin
|
||||||
<p>Debug : Test Modals Gacha</p>
|
isOpen={showModalLogin}
|
||||||
<Button onClick={
|
onClose={() => setShowModalLogin(false)}
|
||||||
() => setShowModal(true)
|
onForgotPassword={handleForgotPasswordClick}
|
||||||
}>Tekan</Button>
|
key="login"
|
||||||
</div>
|
/>
|
||||||
{showModal && createPortal(
|
|
||||||
<ModalsGacha></ModalsGacha>,
|
{/* Register Modal */}
|
||||||
document.body
|
<ModalFormRegister
|
||||||
)}
|
isOpen={showModalRegister}
|
||||||
{/**Izin pake untuk debug modals gacha */}
|
onClose={() => {
|
||||||
|
setShowModalRegister(false);
|
||||||
|
}}
|
||||||
|
key="register"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Forgot Password Modal */}
|
||||||
|
<ModalFormForgotPassword
|
||||||
|
isOpen={showModalForgotPassword}
|
||||||
|
onClose={() => {
|
||||||
|
setShowModalForgotPassword(false);
|
||||||
|
setShowModalLogin(true);
|
||||||
|
}}
|
||||||
|
key="forgot-password"
|
||||||
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -17,59 +17,55 @@
|
|||||||
|
|
||||||
--color-neutral-50: #f6f6f6;
|
--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-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;
|
||||||
@@ -137,4 +133,4 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"@nx/react/babel",
|
||||||
|
{
|
||||||
|
"runtime": "automatic",
|
||||||
|
"useBuiltIns": "usage"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"plugins": []
|
||||||
|
}
|
||||||
@@ -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/).
|
||||||
@@ -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: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { api } from '@imphnen-frontend-service/utils';
|
||||||
|
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;
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './auth';
|
||||||
|
export * from './gacha';
|
||||||
|
export * from './users';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { useMutation, UseMutationResult } from '@tanstack/react-query';
|
||||||
|
import { postLogin, postRegister, postVerifyEmail } from '../../api/auth';
|
||||||
|
import {
|
||||||
|
TLoginRequest,
|
||||||
|
TLoginResponse,
|
||||||
|
TRegisterRequest,
|
||||||
|
TVerifyEmailRequest,
|
||||||
|
} from '../../types/auth';
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './auth';
|
||||||
|
export * from './gacha';
|
||||||
|
export * from './users';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './api';
|
||||||
|
export * from './hooks';
|
||||||
|
export * from './types';
|
||||||