Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
340bd8c4bf | ||
|
|
88c71ccdfa | ||
|
|
5b3b9e44fa | ||
|
|
ac2ff2ec92 | ||
|
|
79e0d39280 | ||
|
|
fd9113109c | ||
|
|
da7e1067d2 | ||
|
|
75779b0373 | ||
|
|
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 |
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<svg width="317" height="170" viewBox="0 0 317 170" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<rect width="317" height="169.613" fill="url(#pattern0_164_317)"/>
|
||||||
|
<defs>
|
||||||
|
<pattern id="pattern0_164_317" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||||
|
<use xlink:href="#image0_164_317" transform="scale(0.00184502 0.00344828)"/>
|
||||||
|
</pattern>
|
||||||
|
<image id="image0_164_317" width="542" height="290" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAh4AAAEiCAYAAAC2k9sBAAAABGdBTUEAALGPC/xhBQAACklpQ0NQc1JHQiBJRUM2MTk2Ni0yLjEAAEiJnVN3WJP3Fj7f92UPVkLY8LGXbIEAIiOsCMgQWaIQkgBhhBASQMWFiApWFBURnEhVxILVCkidiOKgKLhnQYqIWotVXDjuH9yntX167+3t+9f7vOec5/zOec8PgBESJpHmomoAOVKFPDrYH49PSMTJvYACFUjgBCAQ5svCZwXFAADwA3l4fnSwP/wBr28AAgBw1S4kEsfh/4O6UCZXACCRAOAiEucLAZBSAMguVMgUAMgYALBTs2QKAJQAAGx5fEIiAKoNAOz0ST4FANipk9wXANiiHKLine truncated
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 351 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<svg width="138" height="52" viewBox="0 0 138 52" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<rect width="137.505" height="51.7538" fill="url(#pattern0_164_316)"/>
|
||||||
|
<defs>
|
||||||
|
<pattern id="pattern0_164_316" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||||
|
<use xlink:href="#image0_164_316" transform="matrix(0.000440723 0 0 0.00117096 -0.173204 -1.3185)"/>
|
||||||
|
</pattern>
|
||||||
|
<image id="image0_164_316" width="3000" height="3000" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAC7gAAAu4CAYAAABxFfV5AAAACXBIWXMAAC4jAAAuIwF4pT92AAX6AElEQVR4nOzaMQEAIAzAsIF/z0MGPRIFFdCzuwMAAAAAAAAAAAAAAL/d3wEAAAAAAAAAAAAAADBjcAcAAAAAAAAAAAAAIMLgDgAAAAAAAAAAAABAgsEdAAAAAAAAAAAAAIAEgzsAAAAAAAAAAAAAAAkGdwAAAAAAAAAAAAAAEgzuAAAAAAAAAAAAAAAkGNwBAAAAAAAAAAAAAEgwuAMAAAAAAAAAAAAAkGBwBwAAAAAAAAAAAAAgweAOAAAAAAAAAAAAAECCwR0AAAAAAAAAAAAAgASDOwAAAAAAAAAAAAAACQZ3AAAALine truncated
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
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] mx-auto">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { FC, ReactElement } 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 {
|
||||||
|
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 Indonesia',
|
||||||
|
}));
|
||||||
|
|
||||||
|
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 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-md 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>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
disabled
|
||||||
|
size="md"
|
||||||
|
className="flex items-center gap-3"
|
||||||
|
>
|
||||||
|
<FilterOutlined />
|
||||||
|
Filters
|
||||||
|
</Button>
|
||||||
|
</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] mx-auto">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { ArrowDownOutlined, PlusOutlined } 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-md 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">
|
||||||
|
{/* 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>
|
||||||
|
</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-neutral-500 p-0 font-normal hover:bg-transparent hover:text-red-500"
|
||||||
|
>
|
||||||
|
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.png"
|
||||||
|
alt="Lanyard IMPHNEN"
|
||||||
|
className="h-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right-side illustration */}
|
||||||
|
<img
|
||||||
|
src="gacha.png"
|
||||||
|
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] mx-auto">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { FC, ReactElement } 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 {
|
||||||
|
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<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: 'Nomor Transaksi',
|
||||||
|
accessorKey: 'transactionNumber',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Order Valid?',
|
||||||
|
accessorKey: 'status',
|
||||||
|
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',
|
||||||
|
};
|
||||||
|
const statusText: Record<TransactionStatus, string> = {
|
||||||
|
valid: 'Valid',
|
||||||
|
invalid: 'Invalid',
|
||||||
|
unchecked: 'Unchecked',
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`py-1 px-3 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 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-md 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>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="md"
|
||||||
|
className="flex items-center gap-3"
|
||||||
|
>
|
||||||
|
<FilterOutlined />
|
||||||
|
Filters
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<DataTable data={mockTransactions} columns={columns} table={table} />
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -27,49 +27,45 @@
|
|||||||
--color-neutral-900: #2f2f2f;
|
--color-neutral-900: #2f2f2f;
|
||||||
--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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+54
-13
@@ -1,9 +1,9 @@
|
|||||||
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 { InputForm, Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||||
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 { Link } from 'react-router';
|
||||||
|
|
||||||
interface GachaItemProps {
|
interface GachaItemProps {
|
||||||
src: string;
|
src: string;
|
||||||
@@ -14,7 +14,6 @@ interface GachaItemProps {
|
|||||||
export const Components: FC = (): ReactElement => {
|
export const Components: FC = (): ReactElement => {
|
||||||
// Pinjem
|
// Pinjem
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
//
|
|
||||||
|
|
||||||
const scrollToRoulette = () => {
|
const scrollToRoulette = () => {
|
||||||
const rouletteSection = document.getElementById('roulette');
|
const rouletteSection = document.getElementById('roulette');
|
||||||
@@ -222,16 +221,58 @@ export const Components: FC = (): ReactElement => {
|
|||||||
<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 */}
|
{/**Izin pake untuk debug modals gacha */}
|
||||||
<div>
|
<button onClick={() => setShowModal(true)}>Open Modal</button>
|
||||||
<p>Debug : Test Modals Gacha</p>
|
<Modal
|
||||||
<Button onClick={
|
className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7"
|
||||||
() => setShowModal(true)
|
isOpen={showModal}
|
||||||
}>Tekan</Button>
|
onClose={() => setShowModal(false)}
|
||||||
</div>
|
>
|
||||||
{showModal && createPortal(
|
<Modal.Header className="space-y-4">
|
||||||
<ModalsGacha></ModalsGacha>,
|
<img src="/logos/logo.svg" alt="" className="h-[70px] w-auto" />
|
||||||
document.body
|
<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 */}
|
{/**Izin pake untuk debug modals gacha */}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
+37
-41
@@ -27,49 +27,45 @@
|
|||||||
--color-neutral-900: #2f2f2f;
|
--color-neutral-900: #2f2f2f;
|
||||||
--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';
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
export type TLoginRequest = {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TLoginResponse = {
|
||||||
|
data: {
|
||||||
|
token: {
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
};
|
||||||
|
user: {
|
||||||
|
fullname: string;
|
||||||
|
email: string;
|
||||||
|
is_active: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TRegisterRequest = {
|
||||||
|
fullname: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TVerifyEmailRequest = {
|
||||||
|
email: string;
|
||||||
|
otp: number;
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { AxiosError } from 'axios';
|
||||||
|
|
||||||
|
export type TResponse<T = unknown> = {
|
||||||
|
data: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TResponseMessage = {
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TResponseError = AxiosError<TResponseMessage>;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './auth';
|
||||||
|
export * from './gacha';
|
||||||
|
export * from './users';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": false,
|
||||||
|
"esModuleInterop": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"types": ["vite/client", "vitest"]
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.lib.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extends": "../../tsconfig.base.json"
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"types": [
|
||||||
|
"node",
|
||||||
|
"@nx/react/typings/cssmodule.d.ts",
|
||||||
|
"@nx/react/typings/image.d.ts",
|
||||||
|
"vite/client"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/*.test.ts",
|
||||||
|
"**/*.spec.tsx",
|
||||||
|
"**/*.test.tsx",
|
||||||
|
"**/*.spec.js",
|
||||||
|
"**/*.test.js",
|
||||||
|
"**/*.spec.jsx",
|
||||||
|
"**/*.test.jsx",
|
||||||
|
"vite.config.ts",
|
||||||
|
"vite.config.mts",
|
||||||
|
"vitest.config.ts",
|
||||||
|
"vitest.config.mts",
|
||||||
|
"src/**/*.test.ts",
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.test.tsx",
|
||||||
|
"src/**/*.spec.tsx",
|
||||||
|
"src/**/*.test.js",
|
||||||
|
"src/**/*.spec.js",
|
||||||
|
"src/**/*.test.jsx",
|
||||||
|
"src/**/*.spec.jsx"
|
||||||
|
],
|
||||||
|
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"types": [
|
||||||
|
"vitest/globals",
|
||||||
|
"vitest/importMeta",
|
||||||
|
"vite/client",
|
||||||
|
"node",
|
||||||
|
"vitest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"vite.config.ts",
|
||||||
|
"vite.config.mts",
|
||||||
|
"vitest.config.ts",
|
||||||
|
"vitest.config.mts",
|
||||||
|
"src/**/*.test.ts",
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.test.tsx",
|
||||||
|
"src/**/*.spec.tsx",
|
||||||
|
"src/**/*.test.js",
|
||||||
|
"src/**/*.spec.js",
|
||||||
|
"src/**/*.test.jsx",
|
||||||
|
"src/**/*.spec.jsx",
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/// <reference types='vitest' />
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import dts from 'vite-plugin-dts';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||||
|
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
||||||
|
|
||||||
|
export default defineConfig(() => ({
|
||||||
|
root: __dirname,
|
||||||
|
cacheDir: '../../node_modules/.vite/libs/service',
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
nxViteTsPaths(),
|
||||||
|
nxCopyAssetsPlugin(['*.md']),
|
||||||
|
dts({
|
||||||
|
entryRoot: 'src',
|
||||||
|
tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
// Uncomment this if you are using workers.
|
||||||
|
// worker: {
|
||||||
|
// plugins: [ nxViteTsPaths() ],
|
||||||
|
// },
|
||||||
|
// Configuration for building your library.
|
||||||
|
// See: https://vitejs.dev/guide/build.html#library-mode
|
||||||
|
build: {
|
||||||
|
outDir: '../../dist/libs/service',
|
||||||
|
emptyOutDir: true,
|
||||||
|
reportCompressedSize: true,
|
||||||
|
commonjsOptions: {
|
||||||
|
transformMixedEsModules: true,
|
||||||
|
},
|
||||||
|
lib: {
|
||||||
|
// Could also be a dictionary or array of multiple entry points.
|
||||||
|
entry: 'src/index.ts',
|
||||||
|
name: 'service',
|
||||||
|
fileName: 'index',
|
||||||
|
// Change this to the formats you want to support.
|
||||||
|
// Don't forget to update your package.json as well.
|
||||||
|
formats: ['es' as const],
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
// External packages that should not be bundled into your library.
|
||||||
|
external: ['react', 'react-dom', 'react/jsx-runtime'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
watch: false,
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||||
|
reporters: ['default'],
|
||||||
|
coverage: {
|
||||||
|
reportsDirectory: '../../coverage/libs/service',
|
||||||
|
provider: 'v8' as const,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
export * from './button';
|
export * from './button';
|
||||||
export * from './input';
|
export * from './input';
|
||||||
export * from './password-input';
|
|
||||||
export * from './textarea';
|
export * from './textarea';
|
||||||
@@ -1,36 +1,15 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from 'vitest';
|
||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from '@testing-library/react';
|
||||||
import { Input } from "./input";
|
import { Input } from './input';
|
||||||
|
|
||||||
describe("Test Input Component", () => {
|
describe('Test Input Component', () => {
|
||||||
it("renders the input with placeholder text", () => {
|
it('renders the input with placeholder text', () => {
|
||||||
render(<Input placeholder="Placeholder" />);
|
render(<Input placeholder="Placeholder" />);
|
||||||
expect(screen.getByPlaceholderText("Placeholder")).toBeInTheDocument();
|
expect(screen.getByPlaceholderText('Placeholder')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
// it("renders the input with different sizes", () => {
|
|
||||||
// const sizeClasses = {
|
|
||||||
// sm: "text-[10px] max-h-[28px]",
|
|
||||||
// md: "text-[12px] max-h-[30px]",
|
|
||||||
// lg: "text-[15px] max-h-[34px]",
|
|
||||||
// };
|
|
||||||
// Object.entries(sizeClasses).forEach(([size, className]) => {
|
|
||||||
// render(<Input size={size} />);
|
|
||||||
// expect(screen.getByRole("textbox")).toHaveClass(className);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|
||||||
// it("renders the input with different types", () => {
|
|
||||||
// const types = ["text", "password", "email", "number"];
|
|
||||||
// types.forEach((type) => {
|
|
||||||
// render(<Input type={type} />);
|
|
||||||
// const input = screen.getByRole("textbox");
|
|
||||||
// expect(input).toHaveAttribute("type", type);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|
||||||
it("disables the input field when 'disabled' prop is set", () => {
|
it("disables the input field when 'disabled' prop is set", () => {
|
||||||
render(<Input disabled />);
|
render(<Input disabled />);
|
||||||
expect(screen.getByRole("textbox")).toBeDisabled();
|
expect(screen.getByRole('textbox')).toBeDisabled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -4,17 +4,8 @@ import { Input } from './input';
|
|||||||
const meta: Meta<typeof Input> = {
|
const meta: Meta<typeof Input> = {
|
||||||
title: 'Components/Input',
|
title: 'Components/Input',
|
||||||
component: Input,
|
component: Input,
|
||||||
argTypes: {
|
tags: ['autodocs'],
|
||||||
type: {
|
} satisfies Meta<typeof Input>;
|
||||||
control: 'select',
|
|
||||||
options: ['text', 'email'],
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
control: 'select',
|
|
||||||
options: ['sm', 'md', 'lg'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof Input>;
|
type Story = StoryObj<typeof Input>;
|
||||||
@@ -22,6 +13,7 @@ type Story = StoryObj<typeof Input>;
|
|||||||
export const Large: Story = {
|
export const Large: Story = {
|
||||||
args: {
|
args: {
|
||||||
size: 'lg',
|
size: 'lg',
|
||||||
|
type: 'text',
|
||||||
placeholder: 'Placeholder',
|
placeholder: 'Placeholder',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -29,6 +21,7 @@ export const Large: Story = {
|
|||||||
export const Medium: Story = {
|
export const Medium: Story = {
|
||||||
args: {
|
args: {
|
||||||
size: 'md',
|
size: 'md',
|
||||||
|
type: 'text',
|
||||||
placeholder: 'Placeholder',
|
placeholder: 'Placeholder',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -36,6 +29,7 @@ export const Medium: Story = {
|
|||||||
export const Small: Story = {
|
export const Small: Story = {
|
||||||
args: {
|
args: {
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
|
type: 'text',
|
||||||
placeholder: 'Placeholder',
|
placeholder: 'Placeholder',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -54,6 +48,13 @@ export const EmailInput: Story = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PasswordInput: Story = {
|
||||||
|
args: {
|
||||||
|
size: 'md',
|
||||||
|
type: 'password',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const Disabled: Story = {
|
export const Disabled: Story = {
|
||||||
args: {
|
args: {
|
||||||
size: 'md',
|
size: 'md',
|
||||||
@@ -61,11 +62,3 @@ export const Disabled: Story = {
|
|||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithError: Story = {
|
|
||||||
args: {
|
|
||||||
size: 'md',
|
|
||||||
type: 'text',
|
|
||||||
error: 'This field is required',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -3,10 +3,13 @@ import {
|
|||||||
FC,
|
FC,
|
||||||
InputHTMLAttributes,
|
InputHTMLAttributes,
|
||||||
ReactElement,
|
ReactElement,
|
||||||
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import { EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons'; // Import Ant Design icons
|
||||||
import { cn } from '@imphnen-frontend-service/utils';
|
import { cn } from '@imphnen-frontend-service/utils';
|
||||||
|
import { Button } from '../button';
|
||||||
|
|
||||||
type TInputType = 'text' | 'email';
|
type TInputType = 'text' | 'email' | 'password';
|
||||||
type TInputSize = 'sm' | 'md' | 'lg';
|
type TInputSize = 'sm' | 'md' | 'lg';
|
||||||
|
|
||||||
type TInputProps = Omit<
|
type TInputProps = Omit<
|
||||||
@@ -15,46 +18,70 @@ type TInputProps = Omit<
|
|||||||
> & {
|
> & {
|
||||||
type?: TInputType;
|
type?: TInputType;
|
||||||
size?: TInputSize;
|
size?: TInputSize;
|
||||||
error?: string;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeClasses: Record<TInputSize, string> = {
|
const sizeClasses: Record<TInputSize, { textSize: string; iconSize: string }> =
|
||||||
sm: 'text-[10px] max-h-[28px]',
|
{
|
||||||
md: 'text-[12px] max-h-[30px]',
|
sm: { textSize: 'text-[10px] max-h-[28px]', iconSize: 'text-[10px]' },
|
||||||
lg: 'text-[15px] max-h-[34px]',
|
md: { textSize: 'text-[12px] max-h-[30px]', iconSize: 'text-[12px]' },
|
||||||
};
|
lg: { textSize: 'text-[15px] max-h-[34px]', iconSize: 'text-[15px]' },
|
||||||
|
};
|
||||||
|
|
||||||
const disabledClass = 'opacity-50 hover:border-neutral-200 cursor-not-allowed';
|
const disabledClass = 'opacity-50 hover:border-neutral-200 cursor-not-allowed';
|
||||||
const errorClass =
|
|
||||||
'border-danger-500 hover:border-danger-500 focus:outline-danger-500';
|
|
||||||
|
|
||||||
export const Input: FC<TInputProps> = ({
|
export const Input: FC<TInputProps> = ({
|
||||||
|
type = 'text',
|
||||||
size = 'md',
|
size = 'md',
|
||||||
type,
|
|
||||||
placeholder = 'Placeholder',
|
placeholder = 'Placeholder',
|
||||||
disabled,
|
disabled,
|
||||||
error,
|
|
||||||
className,
|
className,
|
||||||
...rest
|
...rest
|
||||||
}): ReactElement => {
|
}): ReactElement => {
|
||||||
|
const [showPassword, setShowPassword] = useState(false); // State for password visibility
|
||||||
|
|
||||||
|
const togglePasswordVisibility = () => {
|
||||||
|
if (!disabled) setShowPassword((prev) => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
const mergedClassName = cn(
|
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',
|
'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',
|
||||||
sizeClasses[size],
|
sizeClasses[size].textSize,
|
||||||
disabled && disabledClass,
|
disabled && disabledClass,
|
||||||
error && errorClass,
|
|
||||||
className
|
className
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="relative flex items-center">
|
||||||
<input
|
<input
|
||||||
className={mergedClassName}
|
className={mergedClassName}
|
||||||
type={type}
|
type={type === 'password' && showPassword ? 'text' : type}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
{error && <p className="text-danger-500 text-xs mt-1">{error}</p>}
|
{type === 'password' && (
|
||||||
</>
|
<div className="absolute end-0 px-[12px] h-full flex items-center">
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
size={size}
|
||||||
|
onClick={togglePasswordVisibility}
|
||||||
|
className={cn(
|
||||||
|
'relative aspect-square -me-[8px] p-[6px]',
|
||||||
|
sizeClasses[size].iconSize,
|
||||||
|
disabled && 'cursor-not-allowed'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<EyeInvisibleOutlined
|
||||||
|
style={{ color: 'var(--color-neutral-500)' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<EyeOutlined style={{ color: 'var(--color-neutral-500)' }} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from "./password-input";
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { Meta, StoryObj } from '@storybook/react';
|
|
||||||
import { PasswordInput } from './password-input';
|
|
||||||
|
|
||||||
const meta: Meta<typeof PasswordInput> = {
|
|
||||||
title: 'Components/Password Input',
|
|
||||||
component: PasswordInput,
|
|
||||||
argTypes: {
|
|
||||||
size: {
|
|
||||||
control: 'select',
|
|
||||||
options: ['sm', 'md', 'lg'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof PasswordInput>;
|
|
||||||
|
|
||||||
export const Large: Story = {
|
|
||||||
args: {
|
|
||||||
size: 'lg',
|
|
||||||
placeholder: 'Placeholder',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Medium: Story = {
|
|
||||||
args: {
|
|
||||||
size: 'md',
|
|
||||||
placeholder: 'Placeholder',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Small: Story = {
|
|
||||||
args: {
|
|
||||||
size: 'sm',
|
|
||||||
placeholder: 'Placeholder',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Input: Story = {
|
|
||||||
args: {
|
|
||||||
size: 'md',
|
|
||||||
placeholder: 'Placeholder',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Disabled: Story = {
|
|
||||||
args: {
|
|
||||||
size: 'md',
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WithError: Story = {
|
|
||||||
args: {
|
|
||||||
size: 'md',
|
|
||||||
error: 'This field is required',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import {
|
|
||||||
DetailedHTMLProps,
|
|
||||||
FC,
|
|
||||||
InputHTMLAttributes,
|
|
||||||
ReactElement,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { cn } from '@imphnen-frontend-service/utils';
|
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons';
|
|
||||||
|
|
||||||
type TPasswordInputType = 'password';
|
|
||||||
type TPasswordInputSize = 'sm' | 'md' | 'lg';
|
|
||||||
|
|
||||||
type TPasswordInputProps = Omit<
|
|
||||||
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
|
|
||||||
'size' | 'type'
|
|
||||||
> & {
|
|
||||||
type?: TPasswordInputType;
|
|
||||||
size?: TPasswordInputSize;
|
|
||||||
error?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const sizeClasses: Record<TPasswordInputSize, string> = {
|
|
||||||
sm: 'text-[10px] max-h-[28px]',
|
|
||||||
md: 'text-[12px] max-h-[30px]',
|
|
||||||
lg: 'text-[15px] max-h-[34px]',
|
|
||||||
};
|
|
||||||
|
|
||||||
const iconSizeClasses: Record<TPasswordInputSize, string> = {
|
|
||||||
sm: 'text-[10px]',
|
|
||||||
md: 'text-[12px]',
|
|
||||||
lg: 'text-[16px]',
|
|
||||||
};
|
|
||||||
|
|
||||||
const disabledClass = 'opacity-50 hover:border-neutral-200 cursor-not-allowed';
|
|
||||||
const errorClass =
|
|
||||||
'border-danger-500 hover:border-danger-500 focus:outline-danger-500';
|
|
||||||
|
|
||||||
export const PasswordInput: FC<TPasswordInputProps> = ({
|
|
||||||
size = 'md',
|
|
||||||
placeholder = 'Placeholder',
|
|
||||||
disabled,
|
|
||||||
error,
|
|
||||||
className,
|
|
||||||
...rest
|
|
||||||
}): ReactElement => {
|
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
|
||||||
|
|
||||||
const togglePasswordVisibility = () => {
|
|
||||||
if (!disabled) setShowPassword((prev) => !prev);
|
|
||||||
};
|
|
||||||
|
|
||||||
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',
|
|
||||||
sizeClasses[size],
|
|
||||||
disabled && disabledClass,
|
|
||||||
error && errorClass,
|
|
||||||
className
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'relative inline-flex items-center border border-neutral-200 rounded-md',
|
|
||||||
mergedClassName
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
className="focus:outline-0"
|
|
||||||
type={showPassword ? 'text' : 'password'}
|
|
||||||
disabled={disabled}
|
|
||||||
placeholder={placeholder}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
<div className="absolute end-0 px-[12px] h-full flex items-center">
|
|
||||||
<Button
|
|
||||||
variant="text"
|
|
||||||
size={size}
|
|
||||||
onClick={togglePasswordVisibility}
|
|
||||||
className={cn(
|
|
||||||
'relative aspect-square -me-[10px] p-[6px]',
|
|
||||||
iconSizeClasses[size],
|
|
||||||
disabled && 'cursor-not-allowed'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{showPassword ? (
|
|
||||||
<EyeInvisibleOutlined
|
|
||||||
style={{
|
|
||||||
color: error
|
|
||||||
? 'var(--color-danger-500)'
|
|
||||||
: 'var(--color-neutral-500)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<EyeOutlined
|
|
||||||
style={{
|
|
||||||
color: error
|
|
||||||
? 'var(--color-danger-500)'
|
|
||||||
: 'var(--color-neutral-500)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{error && <p className="text-danger-500 text-xs mt-1">{error}</p>}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+73
-57
@@ -1,19 +1,21 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=Bai+Jamjuree:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Bai+Jamjuree:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&display=swap');
|
||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
|
@source "../../../libs/ui/**/*.{ts,tsx}";
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--color-primary-50: #f0f8ff;
|
--color-primary-50: #f0f8ff;
|
||||||
--color-primary-100: #e1f0fd;
|
--color-primary-100: #e1f0fd;
|
||||||
--color-primary-200: #b8e1f8;
|
--color-primary-200: #bce1fb;
|
||||||
--color-primary-300: #a1d4f0;
|
--color-primary-300: #81cbf8;
|
||||||
--color-primary-400: #4aa4da;
|
--color-primary-400: #3eb0f2;
|
||||||
--color-primary-500: #1a8ce6;
|
--color-primary-500: #23a1eb;
|
||||||
--color-primary-600: #1673c7;
|
--color-primary-600: #0877c1;
|
||||||
--color-primary-700: #0f5ca5;
|
--color-primary-700: #085f9c;
|
||||||
--color-primary-800: #0a4780;
|
--color-primary-800: #0b5181;
|
||||||
--color-primary-900: #04305d;
|
--color-primary-900: #0f446b;
|
||||||
|
--color-primary-950: #0a2b47;
|
||||||
|
|
||||||
--color-neutral-50: #f7f7f7;
|
--color-neutral-50: #f6f6f6;
|
||||||
--color-neutral-100: #e7e7e7;
|
--color-neutral-100: #e7e7e7;
|
||||||
--color-neutral-200: #d0d0d0;
|
--color-neutral-200: #d0d0d0;
|
||||||
--color-neutral-300: #b7b7b7;
|
--color-neutral-300: #b7b7b7;
|
||||||
@@ -23,50 +25,47 @@
|
|||||||
--color-neutral-700: #4a4a4a;
|
--color-neutral-700: #4a4a4a;
|
||||||
--color-neutral-800: #3a3a3a;
|
--color-neutral-800: #3a3a3a;
|
||||||
--color-neutral-900: #2f2f2f;
|
--color-neutral-900: #2f2f2f;
|
||||||
|
--color-neutral-950: #2b2b2b;
|
||||||
|
|
||||||
--color-success-50: #e7f2ee;
|
--color-success-100: #e0fbd8;
|
||||||
--color-success-100: #cde6dd;
|
--color-success-200: #bcf8b0;
|
||||||
--color-success-200: #9bccbc;
|
--color-success-300: #8eea85;
|
||||||
--color-success-300: #78bdab;
|
--color-success-400: #63d564;
|
||||||
--color-success-400: #4ca88f;
|
--color-success-500: #35ba43;
|
||||||
--color-success-500: #349078;
|
--color-success-600: #269f3e;
|
||||||
--color-success-600: #2f7c66;
|
--color-success-700: #1a8439;
|
||||||
--color-success-700: #26624f;
|
--color-success-800: #106b32;
|
||||||
--color-success-800: #1e4a3b;
|
--color-success-900: #0b592f;
|
||||||
--color-success-900: #1b513b;
|
|
||||||
|
|
||||||
--color-info-50: #e7f4f5;
|
--color-info-100: #ccfcfe;
|
||||||
--color-info-100: #cce9ea;
|
--color-info-200: #9bf3fd;
|
||||||
--color-info-200: #99d3d5;
|
--color-info-300: #67e3fb;
|
||||||
--color-info-300: #78c4c7;
|
--color-info-400: #42cdf8;
|
||||||
--color-info-400: #3aa9ad;
|
--color-info-500: #04acf3;
|
||||||
--color-info-500: #279599;
|
--color-info-600: #0185d0;
|
||||||
--color-info-600: #207d82;
|
--color-info-700: #0264af;
|
||||||
--color-info-700: #1a666b;
|
--color-info-800: #01478d;
|
||||||
--color-info-800: #124c52;
|
--color-info-900: #003375;
|
||||||
--color-info-900: #093d43;
|
|
||||||
|
|
||||||
--color-warning-50: #fffce6;
|
--color-warning-100: #fffcd3;
|
||||||
--color-warning-100: #fff8cc;
|
--color-warning-200: #fffaa9;
|
||||||
--color-warning-200: #fef39b;
|
--color-warning-300: #fff67d;
|
||||||
--color-warning-300: #fde967;
|
--color-warning-400: #fff25d;
|
||||||
--color-warning-400: #fbd93d;
|
--color-warning-500: #ffed27;
|
||||||
--color-warning-500: #f3c215;
|
--color-warning-600: #dbc91d;
|
||||||
--color-warning-600: #d7a20f;
|
--color-warning-700: #b7a714;
|
||||||
--color-warning-700: #aa7e0c;
|
--color-warning-800: #93850b;
|
||||||
--color-warning-800: #805c07;
|
--color-warning-900: #7a6d07;
|
||||||
--color-warning-900: #665c00;
|
|
||||||
|
|
||||||
--color-danger-50: #ffe7e7;
|
--color-danger-100: #ffe8da;
|
||||||
--color-danger-100: #ffcece;
|
--color-danger-200: #ffcbb3;
|
||||||
--color-danger-200: #ff9f9f;
|
--color-danger-300: #ffaa8d;
|
||||||
--color-danger-300: #ff6e6e;
|
--color-danger-400: #ff8870;
|
||||||
--color-danger-400: #fa4646;
|
--color-danger-500: #ff5242;
|
||||||
--color-danger-500: #ef1f1f;
|
--color-danger-600: #da3030;
|
||||||
--color-danger-600: #d11515;
|
--color-danger-700: #b7212d;
|
||||||
--color-danger-700: #a51111;
|
--color-danger-800: #93152a;
|
||||||
--color-danger-800: #7f0c0c;
|
--color-danger-900: #7a0c27;
|
||||||
--color-danger-900: #5d2d2d;
|
|
||||||
|
|
||||||
--radius-none: 0px;
|
--radius-none: 0px;
|
||||||
--radius-sm: 2px;
|
--radius-sm: 2px;
|
||||||
@@ -79,16 +78,28 @@
|
|||||||
|
|
||||||
--font-bai-jamjuree: 'Bai Jamjuree', sans-serif;
|
--font-bai-jamjuree: 'Bai Jamjuree', sans-serif;
|
||||||
|
|
||||||
--text-h1: 3rem; /* ~48px */
|
--text-h1: 3.833rem;
|
||||||
--text-h2: 2.4rem; /* ~38.4px */
|
/* ~46px */
|
||||||
--text-h3: 1.92rem; /* ~30.72px */
|
--text-h2: 3.083rem;
|
||||||
--text-p1: 1.54rem; /* ~24.58px */
|
/* ~37px */
|
||||||
--text-p2: 1.23rem; /* ~19.68px */
|
--text-h3: 2.417rem;
|
||||||
--text-label1: 0.98rem; /* ~15.74px */
|
/* ~29px */
|
||||||
--text-label2: 0.78rem; /* ~12.59px */
|
--text-p1: 1.917rem;
|
||||||
|
/* ~23px */
|
||||||
|
--text-p2: 1.583rem;
|
||||||
|
/* ~19px */
|
||||||
|
--text-p3: 1.25rem;
|
||||||
|
/* 15px */
|
||||||
|
--text-label1: 1rem;
|
||||||
|
/* 12px */
|
||||||
|
--text-label2: 0.833rem;
|
||||||
|
/* ~10px */
|
||||||
|
--text-label3: 0.677rem;
|
||||||
|
/* ~8px */
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
|
|
||||||
*,
|
*,
|
||||||
::after,
|
::after,
|
||||||
::before,
|
::before,
|
||||||
@@ -102,13 +113,16 @@
|
|||||||
width: 6px;
|
width: 6px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: var(--color-neutral-50);
|
background: var(--color-neutral-50);
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #cccccc;
|
background: #cccccc;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: var(--color-neutral-300);
|
background: var(--color-neutral-300);
|
||||||
}
|
}
|
||||||
@@ -116,5 +130,7 @@
|
|||||||
html {
|
html {
|
||||||
font-family: 'Bai Jamjuree', sans-serif;
|
font-family: 'Bai Jamjuree', sans-serif;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1 +1,3 @@
|
|||||||
export {};
|
export * from './input-form';
|
||||||
|
export * from './pagination';
|
||||||
|
export * from './modal';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './input-form';
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import InputForm from './input-form';
|
||||||
|
|
||||||
|
describe('InputForm Component', () => {
|
||||||
|
it('renders correctly with disabled prop', () => {
|
||||||
|
render(<InputForm label="Test Label" disabled={true} />);
|
||||||
|
|
||||||
|
const input = screen.getByLabelText('Test Label');
|
||||||
|
expect(input).toBeDisabled();
|
||||||
|
expect(input).toHaveClass('opacity-50 cursor-not-allowed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correctly without disabled prop', () => {
|
||||||
|
render(<InputForm label="Test Label" disabled={false} />);
|
||||||
|
|
||||||
|
const input = screen.getByLabelText('Test Label');
|
||||||
|
expect(input).not.toBeDisabled();
|
||||||
|
expect(input).not.toHaveClass('opacity-50 cursor-not-allowed');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { InputForm } from './input-form';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Molecules/InputForm',
|
||||||
|
component: InputForm,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component: `
|
||||||
|
Komponen input form yang menggabungkan label dengan kolom input.
|
||||||
|
|
||||||
|
## Aksesibilitas
|
||||||
|
Ketika prop \`htmlFor\` disediakan:
|
||||||
|
- Atribut \`htmlFor\` pada label akan diatur ke nilai tersebut
|
||||||
|
- Atribut \`id\` pada input akan otomatis diatur ke nilai yang sama
|
||||||
|
- Ini menciptakan asosiasi label-input yang tepat untuk aksesibilitas
|
||||||
|
|
||||||
|
Cek dan inspect element pada story With HtmlFor untuk melihat hasilnya.
|
||||||
|
|
||||||
|
## Helper Text dan Error
|
||||||
|
- Jika prop \`error\` disediakan, akan ditampilkan dalam warna merah di bawah input
|
||||||
|
- Jika prop \`helperText\` disediakan dan tidak ada error, akan ditampilkan dalam warna abu-abu di bawah input
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
} satisfies Meta<typeof InputForm>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Large: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Label',
|
||||||
|
placeholder: 'Placeholder',
|
||||||
|
type: 'text',
|
||||||
|
size: 'lg',
|
||||||
|
helperText: 'Helper Text',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Medium: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Label',
|
||||||
|
placeholder: 'Placeholder',
|
||||||
|
type: 'text',
|
||||||
|
size: 'md',
|
||||||
|
helperText: 'Helper Text',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Small: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Label',
|
||||||
|
placeholder: 'Placeholder',
|
||||||
|
type: 'text',
|
||||||
|
size: 'sm',
|
||||||
|
helperText: 'Helper Text',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Default',
|
||||||
|
placeholder: 'Enter your name',
|
||||||
|
type: 'text',
|
||||||
|
size: 'md',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PasswordInput: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Password',
|
||||||
|
placeholder: 'Enter your password',
|
||||||
|
type: 'password',
|
||||||
|
size: 'md',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithHelperText: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Email',
|
||||||
|
placeholder: 'Enter your email',
|
||||||
|
type: 'email',
|
||||||
|
size: 'md',
|
||||||
|
helperText: 'We will never share your email',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithoutHelperText: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Username',
|
||||||
|
placeholder: 'Enter your username',
|
||||||
|
type: 'text',
|
||||||
|
size: 'md',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithError: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Username',
|
||||||
|
placeholder: 'Enter your username',
|
||||||
|
type: 'text',
|
||||||
|
size: 'md',
|
||||||
|
error: 'This field is required',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithHtmlFor: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Full Name',
|
||||||
|
placeholder: 'Enter your full name',
|
||||||
|
type: 'text',
|
||||||
|
size: 'md',
|
||||||
|
htmlFor: 'fullname-input',
|
||||||
|
helperText: 'Click on the label',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Disabled: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Disabled',
|
||||||
|
placeholder: 'This field is disabled',
|
||||||
|
type: 'text',
|
||||||
|
size: 'md',
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
Loaded 50 of 67 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user