Compare commits

..
Author SHA1 Message Date
egagofur 3dda8149ba Merge branch 'develop' into feat/modal-unit-test-and-storybook 2025-03-29 17:56:39 +07:00
egagofur cc21735468 feat: unit test and storybook modal 2025-03-29 17:56:18 +07:00
Maulana SodiqinandGitHub a6bc6de72e Merge pull request #9 from IMPHNEN/feat/gacha-modal-login
feat: add modal login gacha page
2025-03-29 18:33:12 +08:00
egagofur 340bd8c4bf feat: add modal login gacha page 2025-03-29 17:26:54 +07:00
Maulana SodiqinandGitHub 88c71ccdfa Merge pull request #6 from IMPHNEN/backoffice/auth
Backoffice Login, Dashboard, Accounts, Transactions page
2025-03-28 20:05:34 +08:00
Hafid Nur 5b3b9e44fa feat: Implement DataTable to backoffice pages
- Halaman Data Akun
- Halaman Validasi Transaksi
2025-03-28 18:50:54 +07:00
Hafid Nur ac2ff2ec92 feat: Combine Pagination component into DataTable 2025-03-28 18:50:17 +07:00
Hafid Nur 79e0d39280 feat: separate DataTable component 2025-03-28 18:17:13 +07:00
Hafid Nur fd9113109c feat: Working TanStack pagination
- Menerapkan TanStack Table dan Pagination
2025-03-28 17:29:06 +07:00
Hafid Nur da7e1067d2 feat: Implement pagination
- Terapkan pagination pada page Data Akun
2025-03-28 16:50:58 +07:00
Hafid Nur 75779b0373 feat: Simple implementation TanStack table
Coba implementasikan versi paling sederhana TanStack Table v8 langsung ke page Data Akun
2025-03-28 16:21:27 +07:00
Hafid Nur 2ec6ad5b5c feat: Add TanStack Table v8 and implement it to Datatable component 2025-03-28 00:19:01 +07:00
Hafid Nur 2d541c71ba Merge branch into backoffice/auth 2025-03-27 23:19:45 +07:00
Hafid Nur 46f3bd6014 fix: Sidebar stick to left. Fit content for different desktop viewports 2025-03-27 23:17:43 +07:00
Hafid Nur c374c97e66 fix: Sidebar stick to left. Fit content for different desktop viewports 2025-03-27 23:15:11 +07:00
Hafid Nur a4ca53004f feat: fix datatable storybook 2025-03-27 22:52:35 +07:00
Hafid Nur 2d9473ea7c feat: Adjustment
- Sesuaikan color palette success, info, danger, warning sesuai dengan design system Figma
- Sesuai background pagination
- Fix width form login tidak maksimal
2025-03-27 22:28:40 +07:00
Hafid Nur 6bfa48bed8 feat: Change data table component dynamically
- Ubah komponen DataTable agar bisa render menyesuaikan jumlah kolom dan baris pada data
2025-03-27 17:22:04 +07:00
Hafid Nur 43167fe7a1 feat: Draft transaction page 2025-03-27 16:58:24 +07:00
Hafid Nur 39391e5522 feat: Page Data Akun 2025-03-27 16:55:54 +07:00
Hafid Nur ec509a7b43 feat: Styling pagination 2025-03-27 16:41:42 +07:00
Hafid Nur a76bd89ec1 feat: Styling data table component 2025-03-27 16:29:09 +07:00
Hafid Nur cafdb29024 feat: Refactor datatable and pagination component and add storybook
- Refactor nama component dari datatable dan pagination mengikuti codebase
- Buat storybook untuk preview dari masing-masing komponen
2025-03-27 15:59:02 +07:00
Hafid Nur d79d0d3d09 feat: Draft Data Akun page
- Vibes coding untuk siapkan base dari halaman Data Akun
2025-03-27 15:17:04 +07:00
Hafid Nur 7606c5b4e2 feat: Divider on top of logout button sidebar 2025-03-27 15:08:22 +07:00
Hafid Nur bc9a0fd740 feat: Another page template
- Template untuk page Data Akun dan Validasi Transaksi
2025-03-27 15:06:30 +07:00
Hafid Nur 7f9a106fca feat: Fix gacha image preview
- Menggunakan pendekatan simpan gambar clipped langsung daripada mengatur secara dinamis menggunakan CSS object-position
2025-03-27 15:00:04 +07:00
Hafid Nur 0bcc8f7f6a feat: gacha section in dashboard 2025-03-27 14:31:30 +07:00
Hafid Nur 31b7587571 feat: summary section in dashboard page 2025-03-27 13:35:13 +07:00
Hafid Nur 82fb1ea2bc feat: Update sidebar
- Perbaiki tampilan sidebar sesuai desain
2025-03-27 13:18:14 +07:00
Hafid Nur bd5e46e712 feat: Draft dashboard AI-generated 2025-03-27 07:46:15 +07:00
Hafid Nur 542469bc67 feat: Update InputForm, Input, and auth page
- Perbaiki page login ke halaman utama backoffice
- Perbaiki component Input dan InputForm
2025-03-27 07:19:46 +07:00
Hafid Nur 631684cb05 feat: Update InputForm to handle different sizes
- Tambah spec test pada komponen InputForm
- Tambah autodocs pada komponen Input
2025-03-27 06:00:03 +07:00
Hafid Nur 54f4572533 feat: Update InputForm component
- Perbarui docs untuk component Input dan InputForm
- Perbarui component InputForm agar bekerja dengan helper text, error, dan label/input
2025-03-27 05:25:02 +07:00
Hafid Nur ca933f624a feat: Create a separate "InputForm" molecule component
- Develop the "InputForm" molecule component
- Remove empty modal-gacha component stories and test files that prevent Storybook from running
2025-03-27 04:35:41 +07:00
Hafid Nur 90ab974e03 feat: backoffice login page 2025-03-27 02:42:57 +07:00
Maulana Sodiqin daeeb09798 feat: integrate with new api 2025-03-19 23:38:42 +07:00
Maulana SodiqinandGitHub c8935b885e Merge pull request #5 from IMPHNEN/ModalsGacha
Modals gacha
2025-03-17 18:34:21 +07:00
boboiazumi 4ac1278a33 fix: conflict 2025-03-17 18:13:24 +07:00
Maulana Sodiqin d20f0d89e5 feat: init backoffice 2025-03-17 12:48:11 +07:00
Maulana Sodiqin 515b78c57f feat: add backoffice apps 2025-03-17 09:39:04 +07:00
Maulana SodiqinandGitHub 8a53575cd1 Merge pull request #4 from IMPHNEN/feat/simple-landing-page
Roulette page
2025-03-17 09:29:43 +07:00
Hafid Nur c8b12db1b3 feat: linear gradient diffuser 2025-03-16 22:53:13 +07:00
Hafid Nur e3c4b7a23a feat: infinite scroll animation for gacha items 2025-03-16 22:25:29 +07:00
boboiazumi 41a0842026 feat: modals-gacha 2025-03-16 22:18:48 +07:00
Hafid Nur a9164e5e5f feat: gacha item horizontal scroll snap 2025-03-16 22:16:52 +07:00
Hafid Nur d7e39095bb feat: separate GachaItem component and add more Gacha items (images and labels)
TODO:
- implement an infinite scroll snap slider for Gacha items
2025-03-16 21:42:45 +07:00
Hafid Nur 84ddf2b771 feat: draft roulette page 2025-03-16 20:40:18 +07:00
Hafid Nur cb632adbb4 feat: change the layout of the landing page from using CSS Flexbox to CSS Grid
- minor adjustment for navbar max width and padding
2025-03-16 19:54:42 +07:00
Hafid Nur 6a1fa42956 feat: update navbar to cover top of the page
- Update the navbar to be sticky when scrolling the page and cover the top of the page.
- Fix the margins for every viewport.
2025-03-16 19:19:44 +07:00
Hafid Nur 9002e6a261 feat: merge landing page component into single page file
- Component landing page sebelumnya terpisah, tetapi perlu digabungkan lagi karena tidak reusable pada aplikasi
2025-03-16 19:13:49 +07:00
Maulana SodiqinandGitHub 254d4fe04a Merge pull request #2 from IMPHNEN/feat/simple-landing-page
Slicing simple landing page
2025-03-16 12:39:12 +07:00
Hafid Nur 02e06caca1 feat: landing page for desktop viewport 2025-03-16 07:41:16 +07:00
Hafid Nur 00033b9c68 feat: landing page for tablet viewport
- change "scroll for gacha" to button to roulette page
2025-03-16 07:11:06 +07:00
Hafid Nur 02863ebc8b feat: add landing page for mobile viewport 2025-03-16 06:38:14 +07:00
Hafid Nur a31166e3ff feat: update navbar responsive 2025-03-16 06:07:07 +07:00
Hafid Nur c7b74bc9b6 feat: update theme to match the Design System
adjustment
- primary color
- base + typography size
2025-03-16 05:42:24 +07:00
Maulana SodiqinandGitHub 01621a8786 Merge pull request #1 from IMPHNEN/feat/component-migrate
Migrate component from 'imphnen-gacha' repository
2025-03-16 01:45:33 +07:00
hafidnrzs a07e3c21d6 feat: add Textarea component 2025-03-16 01:40:02 +07:00
hafidnrzs fb99b41b3a feat: add PasswordInput component 2025-03-16 01:34:24 +07:00
Hafid Nur 183ca100dc feat: add Input component 2025-03-16 00:56:02 +07:00
Maulana Sodiqin 203ee5de97 chore: remove uneeded folder/file 2025-03-16 00:35:48 +07:00
Maulana Sodiqin 58843041db faet 2025-03-16 00:33:48 +07:00
Maulana Sodiqin 3964c22c66 feat: navbar unit test 2025-03-16 00:16:58 +07:00
Maulana Sodiqin ec02a4ef83 fix: unit test 2025-03-16 00:12:15 +07:00
Maulana Sodiqin 587cb9facf fix: ui error build 2025-03-16 00:07:02 +07:00
Maulana Sodiqin 5c5faaf8a1 fix: libs cannot apply style 2025-03-16 00:05:17 +07:00
117 changed files with 10540 additions and 511 deletions
+3 -1
View File
@@ -42,4 +42,6 @@ Thumbs.db
.nx/workspace-data
vite.config.*.timestamp*
vitest.config.*.timestamp*
vitest.config.*.timestamp*
storybook-static
+12
View File
@@ -0,0 +1,12 @@
import nx from '@nx/eslint-plugin';
import baseConfig from '../../eslint.config.mjs';
export default [
...baseConfig,
...nx.configs['flat/react'],
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
},
];
+16
View File
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Backoffice</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="stylesheet" href="/src/styles.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
import { join } from 'path';
export default {
plugins: {
'@tailwindcss/postcss': {
base: join(import.meta.dirname, '../../'),
},
},
};
+9
View File
@@ -0,0 +1,9 @@
{
"name": "backoffice",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/backoffice/src",
"projectType": "application",
"tags": [],
"// targets": "to see all targets run: nx show project backoffice --web",
"targets": {}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 351 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 511 KiB

@@ -0,0 +1,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;
+159
View File
@@ -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;
+111
View File
@@ -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;
+12
View File
@@ -0,0 +1,12 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
export const AppLayout: FC = (): ReactElement => {
return (
<main className="bg-primary-50 min-h-screen">
<Outlet />
</main>
);
};
export default AppLayout;
+36
View File
@@ -0,0 +1,36 @@
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 => {
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;
@@ -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;
+136
View File
@@ -0,0 +1,136 @@
@import url('https://fonts.googleapis.com/css2?family=Bai+Jamjuree:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&display=swap');
@import 'tailwindcss';
@source "../../../libs/ui/**/*.{ts,tsx}";
@theme {
--color-primary-50: #f0f8ff;
--color-primary-100: #e1f0fd;
--color-primary-200: #bce1fb;
--color-primary-300: #81cbf8;
--color-primary-400: #3eb0f2;
--color-primary-500: #23a1eb;
--color-primary-600: #0877c1;
--color-primary-700: #085f9c;
--color-primary-800: #0b5181;
--color-primary-900: #0f446b;
--color-primary-950: #0a2b47;
--color-neutral-50: #f6f6f6;
--color-neutral-100: #e7e7e7;
--color-neutral-200: #d0d0d0;
--color-neutral-300: #b7b7b7;
--color-neutral-400: #9f9f9f;
--color-neutral-500: #7e7e7e;
--color-neutral-600: #5d5d5d;
--color-neutral-700: #4a4a4a;
--color-neutral-800: #3a3a3a;
--color-neutral-900: #2f2f2f;
--color-neutral-950: #2b2b2b;
--color-success-100: #e0fbd8;
--color-success-200: #bcf8b0;
--color-success-300: #8eea85;
--color-success-400: #63d564;
--color-success-500: #35ba43;
--color-success-600: #269f3e;
--color-success-700: #1a8439;
--color-success-800: #106b32;
--color-success-900: #0b592f;
--color-info-100: #ccfcfe;
--color-info-200: #9bf3fd;
--color-info-300: #67e3fb;
--color-info-400: #42cdf8;
--color-info-500: #04acf3;
--color-info-600: #0185d0;
--color-info-700: #0264af;
--color-info-800: #01478d;
--color-info-900: #003375;
--color-warning-100: #fffcd3;
--color-warning-200: #fffaa9;
--color-warning-300: #fff67d;
--color-warning-400: #fff25d;
--color-warning-500: #ffed27;
--color-warning-600: #dbc91d;
--color-warning-700: #b7a714;
--color-warning-800: #93850b;
--color-warning-900: #7a6d07;
--color-danger-100: #ffe8da;
--color-danger-200: #ffcbb3;
--color-danger-300: #ffaa8d;
--color-danger-400: #ff8870;
--color-danger-500: #ff5242;
--color-danger-600: #da3030;
--color-danger-700: #b7212d;
--color-danger-800: #93152a;
--color-danger-900: #7a0c27;
--radius-none: 0px;
--radius-sm: 2px;
--radius-md: 4px;
--radius-lg: 8px;
--radius-xl: 12px;
--radius-2xl: 16px;
--radius-3xl: 24px;
--radius-full: 50%;
--font-bai-jamjuree: 'Bai Jamjuree', sans-serif;
--text-h1: 3.833rem;
/* ~46px */
--text-h2: 3.083rem;
/* ~37px */
--text-h3: 2.417rem;
/* ~29px */
--text-p1: 1.917rem;
/* ~23px */
--text-p2: 1.583rem;
/* ~19px */
--text-p3: 1.25rem;
/* 15px */
--text-label1: 1rem;
/* 12px */
--text-label2: 0.833rem;
/* ~10px */
--text-label3: 0.677rem;
/* ~8px */
}
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-neutral-200, currentColor);
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: var(--color-neutral-50);
}
::-webkit-scrollbar-thumb {
background: #cccccc;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-neutral-300);
}
html {
font-family: 'Bai Jamjuree', sans-serif;
font-weight: 400;
font-size: 12px;
line-height: 1.2;
}
}
+40
View File
@@ -0,0 +1,40 @@
import { createRoot } from 'react-dom/client';
import { middleware } from './middleware';
import { StrictMode } from 'react';
import { createBrowserRouter, RouteObject, RouterProvider } from 'react-router';
import {
add404PageToRoutesChildren,
addErrorElementToRoutes,
convertPagesToRoute,
QueryProvider,
} from '@imphnen-frontend-service/utils';
import './index.css';
const files = import.meta.glob('./app/**/*(page|layout).tsx');
const errorFiles = import.meta.glob('./app/**/*error.tsx');
const notFoundFiles = import.meta.glob('./app/**/*404.tsx');
const loadingFiles = import.meta.glob('./app/**/*loading.tsx');
const routes = convertPagesToRoute(files, loadingFiles) as RouteObject;
addErrorElementToRoutes(errorFiles, routes);
add404PageToRoutesChildren(notFoundFiles, routes);
const router = createBrowserRouter([
{
...routes,
loader: middleware,
shouldRevalidate: () => true,
},
]);
const rootElement = document.getElementById('root');
if (!rootElement) throw new Error('Failed to find the root element');
createRoot(rootElement).render(
<StrictMode>
<QueryProvider>
<RouterProvider router={router} />
</QueryProvider>
</StrictMode>
);
+11
View File
@@ -0,0 +1,11 @@
import { LoaderFunctionArgs } from 'react-router-dom';
export const middleware = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url);
const pathname = url.pathname;
if (pathname) return null;
return null;
};
+27
View File
@@ -0,0 +1,27 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts",
"vite/client"
]
},
"exclude": [
"src/**/*.spec.ts",
"src/**/*.test.ts",
"src/**/*.spec.tsx",
"src/**/*.test.tsx",
"src/**/*.spec.js",
"src/**/*.test.js",
"src/**/*.spec.jsx",
"src/**/*.test.jsx",
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts"
],
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
}
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"types": ["vite/client", "vitest"]
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../tsconfig.base.json"
}
+30
View File
@@ -0,0 +1,30 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts"
]
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
]
}
+42
View File
@@ -0,0 +1,42 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../node_modules/.vite/apps/backoffice',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [react(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
build: {
outDir: '../../dist/apps/backoffice',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},
test: {
watch: false,
globals: true,
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: '../../coverage/apps/backoffice',
provider: 'v8' as const,
},
},
}));
+2 -2
View File
@@ -6,8 +6,8 @@
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="stylesheet" href="/src/styles.css" />
<link rel="icon" type="image/x-icon" href="/logos/simple.svg" />
<link rel="stylesheet" href="/src/index.css" />
</head>
<body>
<div id="root"></div>
+5 -1
View File
@@ -1,5 +1,9 @@
import { join } from 'path';
export default {
plugins: {
'@tailwindcss/postcss': {},
'@tailwindcss/postcss': {
base: join(import.meta.dirname, '../../'),
},
},
};
+1
View File
@@ -1,5 +1,6 @@
@import url('https://fonts.googleapis.com/css2?family=Bai+Jamjuree:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&display=swap');
@import 'tailwindcss';
@source "../../../libs/ui/**/*.{ts,tsx}";
@theme {
--color-primary-50: #f0f8ff;
+1 -1
View File
@@ -6,7 +6,7 @@
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="icon" type="image/x-icon" href="/logos/simple.svg" />
<link rel="stylesheet" href="/src/index.css" />
</head>
<body>
+5 -1
View File
@@ -1,5 +1,9 @@
import { join } from 'path';
export default {
plugins: {
'@tailwindcss/postcss': {},
'@tailwindcss/postcss': {
base: join(import.meta.dirname, '../../'),
},
},
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

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

After

Width:  |  Height:  |  Size: 4.8 KiB

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

After

Width:  |  Height:  |  Size: 3.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 351 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 511 KiB

After

Width:  |  Height:  |  Size: 511 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

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

After

Width:  |  Height:  |  Size: 1017 B

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

After

Width:  |  Height:  |  Size: 941 B

+1 -1
View File
@@ -4,7 +4,7 @@ import { Navbar } from '@imphnen-frontend-service/ui/organisms';
export const AppLayout: FC = (): ReactElement => {
return (
<main className="bg-primary-50 min-h-screen px-20 py-[60px]">
<main className="bg-primary-50 min-h-screen">
<Navbar />
<Outlet />
</main>
+276 -2
View File
@@ -1,7 +1,281 @@
import { FC, ReactElement } from 'react';
import { ArrowDownOutlined } from '@ant-design/icons';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputForm, Modal } from '@imphnen-frontend-service/ui/molecules';
import { cn } from '@imphnen-frontend-service/utils';
import { FC, Fragment, ReactElement, useEffect, useState } from 'react';
import { Link } from 'react-router';
interface GachaItemProps {
src: string;
label: string;
className?: string;
}
export const Components: FC = (): ReactElement => {
return <>Hallo</>;
// Pinjem
const [showModal, setShowModal] = useState(false);
const scrollToRoulette = () => {
const rouletteSection = document.getElementById('roulette');
if (rouletteSection) {
rouletteSection.scrollIntoView({ behavior: 'smooth' });
}
};
useEffect(() => {
const gachaPlaySection = document.getElementById('gacha-play');
if (gachaPlaySection) {
let currentIndex = 0;
const items = gachaPlaySection.children;
const totalItems = items.length;
const scrollItems = () => {
if (currentIndex >= totalItems) {
currentIndex = 0;
}
items[currentIndex].scrollIntoView({
behavior: 'smooth',
inline: 'center',
});
currentIndex++;
};
const intervalId = setInterval(scrollItems, 2800);
return () => clearInterval(intervalId);
}
}, []);
const GachaItem: FC<GachaItemProps> = ({
src,
label,
className,
}): ReactElement => {
return (
<div className="snap-center flex-auto justify-center items-center flex flex-col min-w-full">
<div className="max-h-[180px] md:max-h-[270px] md:max-w-[500px] mb-2 md:mb-5 flex flex-1 justify-center items-center">
<img
src={src}
alt=""
className={cn('h-[150px] md:h-[255px] object-contain', className)}
/>
</div>
<p className="font-semibold text-center md:text-p2">{label}</p>
</div>
);
};
return (
<Fragment>
{/* Landing Page */}
<section
id="landing-page"
className="my-12 md:my-24 grid grid-cols-4 md:grid-cols-8 lg:grid-cols-12 justify-items-center max-w-[1280px] items-center justify-center mx-[32px] md:mx-[60px] lg:mx-[80px] xl:mx-auto"
>
<div className="order-none lg:order-1 flex flex-col col-span-4 md:col-span-8 lg:col-span-4 md:w-full lg:w-fit items-center gap-7 md:gap-13 text-primary-500 mb-10 md:mb-20">
<img
src="/logos/logo.svg"
alt="IMPHNEN Logo"
className="w-[170px] md:w-[317px]"
/>
<div className="font-semibold text-center">
<div className="text-p1 md:text-h2 lg:font-bold text-center">
<p>New Merchandise</p>
<p>Coming UP</p>
</div>
<div className="text-base md:text-p2 mt-3">
Periode Gacha: 1 - 31 Maret 2025
</div>
</div>
<div className="text-center font-semibold text-p3 md:text-p1">
<p>Let's Go Checkout Our Merch &</p>
<p>Gacha Your Prize Here</p>
</div>
<Button
size="sm"
variant="bordered"
className="flex gap-3 px-3 md:px-5 py-2 md:py-4 bg-primary-100 border border-primary-500 border-dashed rounded md:rounded-md animate-bounce text-label2 md:text-p2"
onClick={scrollToRoulette}
>
<ArrowDownOutlined />
<span>Scroll For Gacha</span>
<ArrowDownOutlined />
</Button>
</div>
<div className="order-1 lg:order-none flex flex-col col-span-4 justify-center items-center mb-8 relative">
<div className="relative h-[150px] md:h-[230px]">
<img
src="/merch/1.png"
alt="Merch 1"
className="relative top-[3px] md:top-[6px] left-[4px] md:left-[7px] z-10 w-[165px] md:w-[266px]"
/>
<img
className="absolute top-0 left-0 min-w-[174px] md:min-w-[280px] z-0"
src="/merch/Vector-1.svg"
alt=""
/>
<img
className="absolute hidden lg:block -bottom-[50px] -right-[50px]"
src="/landing-arrow-1.svg"
alt=""
/>
</div>
<div className="px-3 py-1 text-primary-500 font-semibold text-base md:text-p2 bg-white shadow-md rounded">
Official Merch IMPHNEN
</div>
<div className="mt-2 text-primary-500 font-bold text-p2 md:text-h3">
~ 175k ~
</div>
</div>
<div className="order-2 flex flex-col col-span-4 justify-center items-center mb-8 relative">
<div className="relative h-[130px] md:h-[230px]">
<img
src="/merch/2.png"
alt="Merch 2"
className="relative top-[5px] md:top-[8px] left-0 md:left-[5px] z-10 w-[165px] md:w-[277px]"
/>
<img
className="absolute top-0 -left-[5px] md:-left-[8px] z-0 min-w-[174px] md:min-w-[302px]"
src="/merch/Vector-2.svg"
alt=""
/>
<img
className="absolute hidden lg:block -bottom-[100px] -left-[55px]"
src="/landing-arrow-2.svg"
alt=""
/>
</div>
<div className="px-3 py-1 text-primary-500 font-semibold text-base md:text-p2 bg-white shadow-md rounded">
IMPHNEN Mini Merch
</div>
<div className="mt-2 text-primary-500 font-bold text-p2 md:text-h3">
~ 90k ~
</div>
</div>
</section>
{/* Roulette Page */}
<section
id="roulette"
className="mt-20 pb-50 lg:py-90 mx-[32px] md:mx-[60px] lg:mx-[80px] xl:mx-auto lg:max-w-[1280px] grid grid-cols-4 md:grid-cols-8 lg:grid-cols-12 justify-items-center gap-y-16 md:gap-y-28"
>
<div className="col-span-4 md:col-span-8 lg:col-span-6 text-primary-500 bg-white rounded-lg md:rounded-xl p-5 md:py-9 md:px-13 shadow md:max-w-[430px]">
<header className="mb-4">
<h3 className="font-semibold text-p3 md:text-h2">Gacha Roulette</h3>
</header>
<h4 className="font-semibold my-2 text-base md:text-p1">
How to participate
</h4>
<p className="text-base md:text-p2">
Kamu harus melakukan pembelian merch batch 2 di shopee IMPHNEN
</p>
<h4 className="font-semibold my-2 text-base md:text-p1">
How to gacha
</h4>
<ol className="text-base md:text-p2 list-decimal list-outside ml-4 md:ml-6">
<li>First, press the "Spin Now" button</li>
<li>
If the merch does not match you can reroll by paying IDR5,000
</li>
<li>If it is appropriate you can submit</li>
<li>Then fill in the data for your merch delivery process</li>
</ol>
</div>
<div
id="roulette-spin"
className="col-span-4 md:col-span-8 lg:col-span-6 flex flex-col items-center gap-4 md:gap-8 overflow-x-hidden"
>
<div className="bg-white text-primary-500 font-medium text-p3 md:text-h3 shadow py-2 px-4 md:py-4 md:px-8 max-w-fit rounded-md md:rounded-lg">
Here Take Your Prize
</div>
<section
id="gacha-play"
className="flex flex-nowrap overflow-auto w-full gap-x-8 snap-x snap-mandatory"
>
<GachaItem
src="/gacha/certificate.png"
label="Sertifikat + Laminating"
/>
<GachaItem
src="/gacha/lanyard-id-card.png"
label="Lanyard + ID Card"
/>
<GachaItem src="/gacha/pin.png" label="Pin" />
<GachaItem src="/gacha/sticker.png" label="Sticker Isi 3" />
<GachaItem src="/gacha/sticker.png" label="Sticker Isi 5" />
<GachaItem
src="/gacha/gelang-karet.png"
label="Gelang Karet"
className="h-[86px] md:h-[160px]"
/>
</section>
<Button variant="secondary" size="md">
Spin Now
</Button>
</div>
</section>
{/* Diffuser & Cloud */}
<div className="sticky bottom-0 h-[86px] md:h-[200px] bg-gradient-to-b from-primary-500/0 to-primary-500/50 to-80%"></div>
{/**Izin pake untuk debug modals gacha */}
<button onClick={() => setShowModal(true)}>Open Modal</button>
<Modal
className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7"
isOpen={showModal}
onClose={() => setShowModal(false)}
>
<Modal.Header className="space-y-4">
<img src="/logos/logo.svg" alt="" className="h-[70px] w-auto" />
<h1 className="text-primary-500 text-p1 text-center font-semibold">
Login
</h1>
</Modal.Header>
<Modal.Content className="space-y-4">
<InputForm
label="Email"
placeholder="Masukkan Email"
type="email"
size="lg"
className="w-full"
/>
<InputForm
label="Password"
placeholder="Masukkan password"
type="password"
size="lg"
className="w-full"
/>
<Link to="/forgot-password">
<h1 className="text-end text-primary-500 text-xl font-medium">
Lupa Password?
</h1>
</Link>
</Modal.Content>
<Modal.Footer className="flex flex-col md:flex-col pt-4">
<Button
size="md"
className="w-full"
onClick={() => console.log('Login')}
>
Login
</Button>
<div className="flex justify-center gap-2 pt-2.5 font-medium">
<p className="text-neutral-500">Belum punya akun?</p>
<Link
className="text-primary-500 hover:text-primary-600"
to="/register"
>
Daftar disini
</Link>
</div>
</Modal.Footer>
</Modal>
{/**Izin pake untuk debug modals gacha */}
</Fragment>
);
};
export default Components;
View File
+73 -57
View File
@@ -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 'tailwindcss';
@source "../../../libs/ui/**/*.{ts,tsx}";
@theme {
--color-primary-50: #f0f8ff;
--color-primary-100: #e1f0fd;
--color-primary-200: #b8e1f8;
--color-primary-300: #a1d4f0;
--color-primary-400: #4aa4da;
--color-primary-500: #1a8ce6;
--color-primary-600: #1673c7;
--color-primary-700: #0f5ca5;
--color-primary-800: #0a4780;
--color-primary-900: #04305d;
--color-primary-200: #bce1fb;
--color-primary-300: #81cbf8;
--color-primary-400: #3eb0f2;
--color-primary-500: #23a1eb;
--color-primary-600: #0877c1;
--color-primary-700: #085f9c;
--color-primary-800: #0b5181;
--color-primary-900: #0f446b;
--color-primary-950: #0a2b47;
--color-neutral-50: #f7f7f7;
--color-neutral-50: #f6f6f6;
--color-neutral-100: #e7e7e7;
--color-neutral-200: #d0d0d0;
--color-neutral-300: #b7b7b7;
@@ -23,50 +25,47 @@
--color-neutral-700: #4a4a4a;
--color-neutral-800: #3a3a3a;
--color-neutral-900: #2f2f2f;
--color-neutral-950: #2b2b2b;
--color-success-50: #e7f2ee;
--color-success-100: #cde6dd;
--color-success-200: #9bccbc;
--color-success-300: #78bdab;
--color-success-400: #4ca88f;
--color-success-500: #349078;
--color-success-600: #2f7c66;
--color-success-700: #26624f;
--color-success-800: #1e4a3b;
--color-success-900: #1b513b;
--color-success-100: #e0fbd8;
--color-success-200: #bcf8b0;
--color-success-300: #8eea85;
--color-success-400: #63d564;
--color-success-500: #35ba43;
--color-success-600: #269f3e;
--color-success-700: #1a8439;
--color-success-800: #106b32;
--color-success-900: #0b592f;
--color-info-50: #e7f4f5;
--color-info-100: #cce9ea;
--color-info-200: #99d3d5;
--color-info-300: #78c4c7;
--color-info-400: #3aa9ad;
--color-info-500: #279599;
--color-info-600: #207d82;
--color-info-700: #1a666b;
--color-info-800: #124c52;
--color-info-900: #093d43;
--color-info-100: #ccfcfe;
--color-info-200: #9bf3fd;
--color-info-300: #67e3fb;
--color-info-400: #42cdf8;
--color-info-500: #04acf3;
--color-info-600: #0185d0;
--color-info-700: #0264af;
--color-info-800: #01478d;
--color-info-900: #003375;
--color-warning-50: #fffce6;
--color-warning-100: #fff8cc;
--color-warning-200: #fef39b;
--color-warning-300: #fde967;
--color-warning-400: #fbd93d;
--color-warning-500: #f3c215;
--color-warning-600: #d7a20f;
--color-warning-700: #aa7e0c;
--color-warning-800: #805c07;
--color-warning-900: #665c00;
--color-warning-100: #fffcd3;
--color-warning-200: #fffaa9;
--color-warning-300: #fff67d;
--color-warning-400: #fff25d;
--color-warning-500: #ffed27;
--color-warning-600: #dbc91d;
--color-warning-700: #b7a714;
--color-warning-800: #93850b;
--color-warning-900: #7a6d07;
--color-danger-50: #ffe7e7;
--color-danger-100: #ffcece;
--color-danger-200: #ff9f9f;
--color-danger-300: #ff6e6e;
--color-danger-400: #fa4646;
--color-danger-500: #ef1f1f;
--color-danger-600: #d11515;
--color-danger-700: #a51111;
--color-danger-800: #7f0c0c;
--color-danger-900: #5d2d2d;
--color-danger-100: #ffe8da;
--color-danger-200: #ffcbb3;
--color-danger-300: #ffaa8d;
--color-danger-400: #ff8870;
--color-danger-500: #ff5242;
--color-danger-600: #da3030;
--color-danger-700: #b7212d;
--color-danger-800: #93152a;
--color-danger-900: #7a0c27;
--radius-none: 0px;
--radius-sm: 2px;
@@ -79,16 +78,28 @@
--font-bai-jamjuree: 'Bai Jamjuree', sans-serif;
--text-h1: 3rem; /* ~48px */
--text-h2: 2.4rem; /* ~38.4px */
--text-h3: 1.92rem; /* ~30.72px */
--text-p1: 1.54rem; /* ~24.58px */
--text-p2: 1.23rem; /* ~19.68px */
--text-label1: 0.98rem; /* ~15.74px */
--text-label2: 0.78rem; /* ~12.59px */
--text-h1: 3.833rem;
/* ~46px */
--text-h2: 3.083rem;
/* ~37px */
--text-h3: 2.417rem;
/* ~29px */
--text-p1: 1.917rem;
/* ~23px */
--text-p2: 1.583rem;
/* ~19px */
--text-p3: 1.25rem;
/* 15px */
--text-label1: 1rem;
/* 12px */
--text-label2: 0.833rem;
/* ~10px */
--text-label3: 0.677rem;
/* ~8px */
}
@layer base {
*,
::after,
::before,
@@ -102,13 +113,16 @@
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: var(--color-neutral-50);
}
::-webkit-scrollbar-thumb {
background: #cccccc;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-neutral-300);
}
@@ -116,5 +130,7 @@
html {
font-family: 'Bai Jamjuree', sans-serif;
font-weight: 400;
font-size: 12px;
line-height: 1.2;
}
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
+7
View File
@@ -0,0 +1,7 @@
# service
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test service` to execute the unit tests via [Vitest](https://vitest.dev/).
+12
View File
@@ -0,0 +1,12 @@
import nx from '@nx/eslint-plugin';
import baseConfig from '../../eslint.config.mjs';
export default [
...baseConfig,
...nx.configs['flat/react'],
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
},
];
+12
View File
@@ -0,0 +1,12 @@
{
"name": "@imphnen-frontend-service/service",
"version": "0.0.1",
"main": "./index.js",
"types": "./index.d.ts",
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js"
}
}
}
+23
View File
@@ -0,0 +1,23 @@
{
"name": "service",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/service/src",
"projectType": "library",
"tags": [],
"targets": {
"nx-release-publish": {
"options": {
"packageRoot": "dist/{projectRoot}"
}
}
},
"release": {
"version": {
"generatorOptions": {
"packageRoot": "dist/{projectRoot}",
"currentVersionResolver": "git-tag",
"fallbackCurrentVersionResolver": "disk"
}
}
}
}
+41
View File
@@ -0,0 +1,41 @@
import { api } from '@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;
};
+1
View File
@@ -0,0 +1 @@
export {};
+3
View File
@@ -0,0 +1,3 @@
export * from './auth';
export * from './gacha';
export * from './users';
+1
View File
@@ -0,0 +1 @@
export {};
+45
View File
@@ -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),
});
};
+1
View File
@@ -0,0 +1 @@
export {};
+3
View File
@@ -0,0 +1,3 @@
export * from './auth';
export * from './gacha';
export * from './users';
+1
View File
@@ -0,0 +1 @@
export {};
+3
View File
@@ -0,0 +1,3 @@
export * from './api';
export * from './hooks';
export * from './types';
+29
View File
@@ -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;
};
+11
View File
@@ -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>;
+1
View File
@@ -0,0 +1 @@
export {};
+3
View File
@@ -0,0 +1,3 @@
export * from './auth';
export * from './gacha';
export * from './users';
+1
View File
@@ -0,0 +1 @@
export {};
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"types": ["vite/client", "vitest"]
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../tsconfig.base.json"
}
+35
View File
@@ -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"]
}
+28
View File
@@ -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"
]
}
+59
View File
@@ -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,
},
},
}));
+20
View File
@@ -0,0 +1,20 @@
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.@(mdx|stories.@(js|jsx|ts|tsx))'],
addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'],
framework: {
name: '@storybook/react-vite',
options: {
builder: {
viteConfigPath: 'vite.config.ts',
},
},
},
};
export default config;
// To customize your Vite configuration you can use the viteFinal field.
// Check https://storybook.js.org/docs/react/builders/vite#configuration
// and https://nx.dev/recipes/storybook/custom-builder-configs
+15
View File
@@ -0,0 +1,15 @@
import type { Preview } from '@storybook/react';
import '../src/index.css';
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;
-5
View File
@@ -3,11 +3,6 @@
"version": "0.0.1",
"main": "./index.js",
"types": "./index.d.ts",
"devDependencies": {
"tailwindcss": "^4.0.13",
"postcss": "^8.5.3",
"@tailwindcss/postcss": "^4.0.13"
},
"exports": {
".": {
"import": "./index.mjs",
+2
View File
@@ -1,3 +1,5 @@
import { join } from 'path';
export default {
plugins: {
'@tailwindcss/postcss': {},
+104
View File
@@ -0,0 +1,104 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './button';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'success', 'danger'],
},
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
},
disabled: {
control: 'boolean',
},
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
variant: 'primary',
size: 'md',
children: 'Primary Button',
},
};
export const Secondary: Story = {
args: {
variant: 'secondary',
size: 'md',
children: 'Secondary Button',
},
};
export const Text: Story = {
args: {
variant: 'text',
size: 'md',
children: 'Text Button',
},
};
export const Bordered: Story = {
args: {
variant: 'bordered',
size: 'md',
children: 'Bordered Button',
},
};
export const Success: Story = {
args: {
variant: 'success',
size: 'md',
children: 'Success Button',
},
};
export const Danger: Story = {
args: {
variant: 'danger',
size: 'md',
children: 'Danger Button',
},
};
export const Disabled: Story = {
args: {
variant: 'primary',
size: 'md',
disabled: true,
children: 'Disabled Button',
},
};
export const Large: Story = {
args: {
variant: 'primary',
size: 'lg',
children: 'Large Button',
},
};
export const Medium: Story = {
args: {
variant: 'primary',
size: 'md',
children: 'Medium Button',
},
};
export const Small: Story = {
args: {
variant: 'primary',
size: 'sm',
children: 'Small Button',
},
};
+2 -1
View File
@@ -1,2 +1,3 @@
import '../index.css';
export * from './button';
export * from './input';
export * from './textarea';
+1
View File
@@ -0,0 +1 @@
export * from "./input";
+15
View File
@@ -0,0 +1,15 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { Input } from './input';
describe('Test Input Component', () => {
it('renders the input with placeholder text', () => {
render(<Input placeholder="Placeholder" />);
expect(screen.getByPlaceholderText('Placeholder')).toBeInTheDocument();
});
it("disables the input field when 'disabled' prop is set", () => {
render(<Input disabled />);
expect(screen.getByRole('textbox')).toBeDisabled();
});
});
+64
View File
@@ -0,0 +1,64 @@
import { Meta, StoryObj } from '@storybook/react';
import { Input } from './input';
const meta: Meta<typeof Input> = {
title: 'Components/Input',
component: Input,
tags: ['autodocs'],
} satisfies Meta<typeof Input>;
export default meta;
type Story = StoryObj<typeof Input>;
export const Large: Story = {
args: {
size: 'lg',
type: 'text',
placeholder: 'Placeholder',
},
};
export const Medium: Story = {
args: {
size: 'md',
type: 'text',
placeholder: 'Placeholder',
},
};
export const Small: Story = {
args: {
size: 'sm',
type: 'text',
placeholder: 'Placeholder',
},
};
export const TextInput: Story = {
args: {
size: 'md',
type: 'text',
},
};
export const EmailInput: Story = {
args: {
size: 'md',
type: 'email',
},
};
export const PasswordInput: Story = {
args: {
size: 'md',
type: 'password',
},
};
export const Disabled: Story = {
args: {
size: 'md',
type: 'text',
disabled: true,
},
};
+87
View File
@@ -0,0 +1,87 @@
import {
DetailedHTMLProps,
FC,
InputHTMLAttributes,
ReactElement,
useState,
} from 'react';
import { EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons'; // Import Ant Design icons
import { cn } from '@imphnen-frontend-service/utils';
import { Button } from '../button';
type TInputType = 'text' | 'email' | 'password';
type TInputSize = 'sm' | 'md' | 'lg';
type TInputProps = Omit<
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
'size' | 'type'
> & {
type?: TInputType;
size?: TInputSize;
disabled?: boolean;
};
const sizeClasses: Record<TInputSize, { textSize: string; iconSize: string }> =
{
sm: { textSize: 'text-[10px] max-h-[28px]', iconSize: 'text-[10px]' },
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';
export const Input: FC<TInputProps> = ({
type = 'text',
size = 'md',
placeholder = 'Placeholder',
disabled,
className,
...rest
}): ReactElement => {
const [showPassword, setShowPassword] = useState(false); // State for password visibility
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 min-w-70',
sizeClasses[size].textSize,
disabled && disabledClass,
className
);
return (
<div className="relative flex items-center">
<input
className={mergedClassName}
type={type === 'password' && showPassword ? 'text' : type}
disabled={disabled}
placeholder={placeholder}
{...rest}
/>
{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
View File
@@ -0,0 +1 @@
export * from "./textarea";
@@ -0,0 +1,49 @@
import { Meta, StoryObj } from "@storybook/react";
import { Textarea } from "./textarea";
const meta: Meta = {
title: "Components/Textarea",
component: Textarea,
argTypes: {
size: {
control: "select",
options: ["sm", "md", "lg"],
},
},
};
export default meta;
type Story = StoryObj<typeof Textarea>;
export const Large: Story = {
args: {
size: "lg",
placeholder: "Placeholder",
},
};
export const Medium: Story = {
args: {
size: "md",
},
};
export const Small: Story = {
args: {
size: "sm",
},
};
export const Disabled: Story = {
args: {
size: "md",
disabled: true,
},
};
export const WithError: Story = {
args: {
size: "md",
error: "Error message",
},
};
+59
View File
@@ -0,0 +1,59 @@
import { cn } from '@imphnen-frontend-service/utils';
import {
DetailedHTMLProps,
FC,
ReactElement,
TextareaHTMLAttributes,
} from 'react';
type TTextareaSize = 'sm' | 'md' | 'lg';
type TTextareaProps = Omit<
DetailedHTMLProps<
TextareaHTMLAttributes<HTMLTextAreaElement>,
HTMLTextAreaElement
>,
'size'
> & {
size?: TTextareaSize;
error?: string;
};
const sizeClasses: Record<TTextareaSize, string> = {
sm: 'text-[10px]',
md: 'text-[12px]',
lg: 'text-[15px]',
};
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 Textarea: FC<TTextareaProps> = ({
size = 'md',
placeholder = 'Placeholder',
disabled,
error,
className,
...rest
}): ReactElement => {
const mergedClassName = cn(
'rounded-md border border-neutral-200 hover:border-blue-300 focus:outline-1 focus:outline-blue-500 px-[12px] py-[8px] invalid:border-danger-500 invalid:text-danger-500',
sizeClasses[size],
disabled && disabledClass,
error && errorClass,
className
);
return (
<>
<textarea
className={mergedClassName}
placeholder={placeholder}
disabled={disabled}
style={{ resize: disabled ? 'none' : 'both' }}
{...rest}
></textarea>
{error && <p className="text-danger-500 text-xs">{error}</p>}
</>
);
};
+73 -57
View File
@@ -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 'tailwindcss';
@source "../../../libs/ui/**/*.{ts,tsx}";
@theme {
--color-primary-50: #f0f8ff;
--color-primary-100: #e1f0fd;
--color-primary-200: #b8e1f8;
--color-primary-300: #a1d4f0;
--color-primary-400: #4aa4da;
--color-primary-500: #1a8ce6;
--color-primary-600: #1673c7;
--color-primary-700: #0f5ca5;
--color-primary-800: #0a4780;
--color-primary-900: #04305d;
--color-primary-200: #bce1fb;
--color-primary-300: #81cbf8;
--color-primary-400: #3eb0f2;
--color-primary-500: #23a1eb;
--color-primary-600: #0877c1;
--color-primary-700: #085f9c;
--color-primary-800: #0b5181;
--color-primary-900: #0f446b;
--color-primary-950: #0a2b47;
--color-neutral-50: #f7f7f7;
--color-neutral-50: #f6f6f6;
--color-neutral-100: #e7e7e7;
--color-neutral-200: #d0d0d0;
--color-neutral-300: #b7b7b7;
@@ -23,50 +25,47 @@
--color-neutral-700: #4a4a4a;
--color-neutral-800: #3a3a3a;
--color-neutral-900: #2f2f2f;
--color-neutral-950: #2b2b2b;
--color-success-50: #e7f2ee;
--color-success-100: #cde6dd;
--color-success-200: #9bccbc;
--color-success-300: #78bdab;
--color-success-400: #4ca88f;
--color-success-500: #349078;
--color-success-600: #2f7c66;
--color-success-700: #26624f;
--color-success-800: #1e4a3b;
--color-success-900: #1b513b;
--color-success-100: #e0fbd8;
--color-success-200: #bcf8b0;
--color-success-300: #8eea85;
--color-success-400: #63d564;
--color-success-500: #35ba43;
--color-success-600: #269f3e;
--color-success-700: #1a8439;
--color-success-800: #106b32;
--color-success-900: #0b592f;
--color-info-50: #e7f4f5;
--color-info-100: #cce9ea;
--color-info-200: #99d3d5;
--color-info-300: #78c4c7;
--color-info-400: #3aa9ad;
--color-info-500: #279599;
--color-info-600: #207d82;
--color-info-700: #1a666b;
--color-info-800: #124c52;
--color-info-900: #093d43;
--color-info-100: #ccfcfe;
--color-info-200: #9bf3fd;
--color-info-300: #67e3fb;
--color-info-400: #42cdf8;
--color-info-500: #04acf3;
--color-info-600: #0185d0;
--color-info-700: #0264af;
--color-info-800: #01478d;
--color-info-900: #003375;
--color-warning-50: #fffce6;
--color-warning-100: #fff8cc;
--color-warning-200: #fef39b;
--color-warning-300: #fde967;
--color-warning-400: #fbd93d;
--color-warning-500: #f3c215;
--color-warning-600: #d7a20f;
--color-warning-700: #aa7e0c;
--color-warning-800: #805c07;
--color-warning-900: #665c00;
--color-warning-100: #fffcd3;
--color-warning-200: #fffaa9;
--color-warning-300: #fff67d;
--color-warning-400: #fff25d;
--color-warning-500: #ffed27;
--color-warning-600: #dbc91d;
--color-warning-700: #b7a714;
--color-warning-800: #93850b;
--color-warning-900: #7a6d07;
--color-danger-50: #ffe7e7;
--color-danger-100: #ffcece;
--color-danger-200: #ff9f9f;
--color-danger-300: #ff6e6e;
--color-danger-400: #fa4646;
--color-danger-500: #ef1f1f;
--color-danger-600: #d11515;
--color-danger-700: #a51111;
--color-danger-800: #7f0c0c;
--color-danger-900: #5d2d2d;
--color-danger-100: #ffe8da;
--color-danger-200: #ffcbb3;
--color-danger-300: #ffaa8d;
--color-danger-400: #ff8870;
--color-danger-500: #ff5242;
--color-danger-600: #da3030;
--color-danger-700: #b7212d;
--color-danger-800: #93152a;
--color-danger-900: #7a0c27;
--radius-none: 0px;
--radius-sm: 2px;
@@ -79,16 +78,28 @@
--font-bai-jamjuree: 'Bai Jamjuree', sans-serif;
--text-h1: 3rem; /* ~48px */
--text-h2: 2.4rem; /* ~38.4px */
--text-h3: 1.92rem; /* ~30.72px */
--text-p1: 1.54rem; /* ~24.58px */
--text-p2: 1.23rem; /* ~19.68px */
--text-label1: 0.98rem; /* ~15.74px */
--text-label2: 0.78rem; /* ~12.59px */
--text-h1: 3.833rem;
/* ~46px */
--text-h2: 3.083rem;
/* ~37px */
--text-h3: 2.417rem;
/* ~29px */
--text-p1: 1.917rem;
/* ~23px */
--text-p2: 1.583rem;
/* ~19px */
--text-p3: 1.25rem;
/* 15px */
--text-label1: 1rem;
/* 12px */
--text-label2: 0.833rem;
/* ~10px */
--text-label3: 0.677rem;
/* ~8px */
}
@layer base {
*,
::after,
::before,
@@ -102,13 +113,16 @@
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: var(--color-neutral-50);
}
::-webkit-scrollbar-thumb {
background: #cccccc;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-neutral-300);
}
@@ -116,5 +130,7 @@
html {
font-family: 'Bai Jamjuree', sans-serif;
font-weight: 400;
font-size: 12px;
line-height: 1.2;
}
}
}
+3 -2
View File
@@ -1,2 +1,3 @@
import '../index.css';
export {};
export * from './input-form';
export * from './pagination';
export * from './modal/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,
},
};
@@ -0,0 +1,92 @@
import {
DetailedHTMLProps,
FC,
InputHTMLAttributes,
ReactElement,
} from 'react';
import { Input } from '../../atoms';
import { cn } from '@imphnen-frontend-service/utils';
type TInputType = 'text' | 'email' | 'password';
type TInputSize = 'sm' | 'md' | 'lg';
type TInputFormProps = Omit<
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
'size' | 'type'
> & {
label: string;
type?: TInputType;
size?: TInputSize;
error?: string;
disabled?: boolean;
helperText?: string;
htmlFor?: string;
};
const sizeClasses: Record<TInputSize, { label: string; helperText: string }> = {
lg: {
label: 'text-p3 font-medium',
helperText: 'text-label3 font-normal',
},
md: {
label: 'text-label1 font-medium',
helperText: 'text-label2 font-normal',
},
sm: {
label: 'text-label2 font-medium',
helperText: 'text-label2 font-normal',
},
};
export const InputForm: FC<TInputFormProps> = ({
label,
placeholder,
type = 'text',
size = 'md',
error,
helperText,
htmlFor,
className,
disabled,
...rest
}): ReactElement => {
return (
<div className="flex gap-[8px] flex-col">
<label
htmlFor={htmlFor}
className={cn(
'items-start justify-item-start text-start',
sizeClasses[size].label
)}
>
{label}
</label>
<Input
{...(htmlFor && { id: htmlFor })}
placeholder={placeholder}
type={type}
size={size}
disabled={disabled}
className={cn(
error &&
'border-danger-500 hover:border-danger-500 focus:outline-danger-500',
className,
disabled && 'opacity-50 cursor-not-allowed' // Add styles for disabled state
)}
{...rest}
/>
{error ? (
<p className="text-danger-500 text-xs mt-1">{error}</p>
) : (
helperText && (
<p className={cn('text-cs mt-1', sizeClasses[size].helperText)}>
{helperText}
</p>
)
)}
</div>
);
};
export default InputForm;
+190
View File
@@ -0,0 +1,190 @@
import { CloseOutlined } from '@ant-design/icons';
import { cn } from '@imphnen-frontend-service/utils';
import React, { useEffect, useMemo, useCallback } from 'react';
import { createPortal } from 'react-dom';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
className?: string;
overlayClassName?: string;
closeButtonClassName?: string;
disableEscapeKeyDown?: boolean;
'aria-label'?: string;
'aria-labelledby'?: string;
'aria-describedby'?: string;
}
const Modal = ({
isOpen,
onClose,
children,
className,
overlayClassName,
closeButtonClassName,
disableEscapeKeyDown = false,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
'aria-describedby': ariaDescribedBy,
}: ModalProps) => {
const handleEscapeKey = useCallback(
(event: KeyboardEvent) => {
if (event.key === 'Escape' && isOpen && !disableEscapeKeyDown) {
onClose();
}
},
[isOpen, onClose, disableEscapeKeyDown]
);
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
window.addEventListener('keydown', handleEscapeKey);
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
window.removeEventListener('keydown', handleEscapeKey);
};
}, [isOpen, handleEscapeKey]);
const modalNode = useMemo(() => document.createElement('div'), []);
useEffect(() => {
document.body.appendChild(modalNode);
return () => {
document.body.removeChild(modalNode);
};
}, [modalNode]);
if (!isOpen) return null;
return createPortal(
<div className="fixed inset-0 z-50">
<div
className={cn(
'fixed inset-0 bg-gray-900/80 transition-opacity duration-200',
isOpen ? 'opacity-100' : 'opacity-0',
overlayClassName
)}
onClick={onClose}
role="presentation"
/>
<div
className={cn(
'fixed left-[50%] top-[50%] z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 bg-[#F0F8FF] rounded-lg p-6 shadow-xl transition-all duration-200',
'sm:rounded-lg sm:max-w-md',
isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95',
className
)}
role="dialog"
aria-modal="true"
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
aria-describedby={ariaDescribedBy}
>
{children}
<button
onClick={onClose}
className={cn(
'absolute right-4 top-4 rounded-sm p-1 text-gray-500 transition-colors hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-gray-950 focus:ring-offset-2',
closeButtonClassName
)}
aria-label="Close modal"
>
<CloseOutlined className="h-4 w-4 cursor-pointer" />
</button>
</div>
</div>,
modalNode
);
};
interface ModalHeaderProps {
className?: string;
children: React.ReactNode;
}
const ModalHeader = ({ className, children }: ModalHeaderProps) => (
<div
className={cn(
'mb-4 flex flex-col space-y-1.5 text-center sm:text-left',
className
)}
>
{children}
</div>
);
interface ModalContentProps {
className?: string;
children: React.ReactNode;
}
const ModalContent = ({ className, children }: ModalContentProps) => (
<div className={cn('mb-4', className)}>{children}</div>
);
interface ModalFooterProps {
className?: string;
children: React.ReactNode;
}
const ModalFooter = ({ className, children }: ModalFooterProps) => (
<div className={cn('flex gap-2 sm:flex-row sm:justify-end', className)}>
{children}
</div>
);
interface ModalTitleProps {
className?: string;
children: React.ReactNode;
id?: string;
}
const ModalTitle = ({ className, children, id }: ModalTitleProps) => (
<h2
id={id}
className={cn(
'text-lg font-semibold leading-none tracking-tight',
className
)}
>
{children}
</h2>
);
interface ModalDescriptionProps {
className?: string;
children: React.ReactNode;
id?: string;
}
const ModalDescription = ({
className,
children,
id,
}: ModalDescriptionProps) => (
<p id={id} className={cn('text-sm text-gray-500', className)}>
{children}
</p>
);
Modal.Header = ModalHeader;
Modal.Content = ModalContent;
Modal.Footer = ModalFooter;
Modal.Title = ModalTitle;
Modal.Description = ModalDescription;
export { Modal };
export type {
ModalProps,
ModalHeaderProps,
ModalContentProps,
ModalFooterProps,
ModalTitleProps,
ModalDescriptionProps,
};
+106
View File
@@ -0,0 +1,106 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { Modal } from './modal';
import { vi } from 'vitest';
describe('Modal Component', () => {
const defaultProps = {
isOpen: true,
onClose: vi.fn(),
children: <div>Modal Content</div>,
};
afterEach(() => {
vi.clearAllMocks();
});
it('renders when isOpen is true', () => {
render(<Modal {...defaultProps} />);
expect(screen.getByText('Modal Content')).toBeInTheDocument();
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(screen.getByLabelText('Close modal')).toBeInTheDocument();
});
it('does not render when isOpen is false', () => {
render(<Modal {...defaultProps} isOpen={false} />);
expect(screen.queryByText('Modal Content')).not.toBeInTheDocument();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
it('calls onClose when close button is clicked', async () => {
render(<Modal {...defaultProps} />);
fireEvent.click(screen.getByLabelText('Close modal'));
await waitFor(() => expect(defaultProps.onClose).toHaveBeenCalledTimes(1));
});
it('calls onClose when overlay is clicked', async () => {
render(<Modal {...defaultProps} />);
fireEvent.click(screen.getByRole('presentation'));
await waitFor(() => expect(defaultProps.onClose).toHaveBeenCalledTimes(1));
});
it('closes on Escape key press when disableEscapeKeyDown is false', async () => {
render(<Modal {...defaultProps} />);
fireEvent.keyDown(window, { key: 'Escape' });
await waitFor(() => expect(defaultProps.onClose).toHaveBeenCalledTimes(1));
});
it('does not close on Escape key press when disableEscapeKeyDown is true', async () => {
render(<Modal {...defaultProps} disableEscapeKeyDown={true} />);
fireEvent.keyDown(window, { key: 'Escape' });
await waitFor(() => expect(defaultProps.onClose).not.toHaveBeenCalled());
});
it('renders sub-components correctly', () => {
render(
<Modal {...defaultProps}>
<Modal.Header>
<Modal.Title>Modal Title</Modal.Title>
<Modal.Description>Modal Description</Modal.Description>
</Modal.Header>
<Modal.Content>Modal Content</Modal.Content>
<Modal.Footer>
<button>Footer Button</button>
</Modal.Footer>
</Modal>
);
expect(screen.getByText('Modal Title')).toBeInTheDocument();
expect(screen.getByText('Modal Description')).toBeInTheDocument();
expect(screen.getByText('Modal Content')).toBeInTheDocument();
expect(screen.getByText('Footer Button')).toBeInTheDocument();
});
it('applies custom classNames', () => {
render(
<Modal
{...defaultProps}
className="custom-modal"
overlayClassName="custom-overlay"
closeButtonClassName="custom-close"
/>
);
expect(screen.getByRole('dialog')).toHaveClass('custom-modal');
expect(screen.getByRole('presentation')).toHaveClass('custom-overlay');
expect(screen.getByLabelText('Close modal')).toHaveClass('custom-close');
});
it('sets ARIA attributes correctly', () => {
render(
<Modal
{...defaultProps}
aria-label="Test Modal"
aria-labelledby="modal-title"
aria-describedby="modal-desc"
>
<Modal.Header>
<Modal.Title id="modal-title">Title</Modal.Title>
<Modal.Description id="modal-desc">Description</Modal.Description>
</Modal.Header>
</Modal>
);
const modal = screen.getByRole('dialog');
expect(modal).toHaveAttribute('aria-label', 'Test Modal');
expect(modal).toHaveAttribute('aria-labelledby', 'modal-title');
expect(modal).toHaveAttribute('aria-describedby', 'modal-desc');
});
});
@@ -0,0 +1,84 @@
import { Modal } from './modal';
import { Meta, StoryObj } from '@storybook/react';
const meta: Meta<typeof Modal> = {
title: 'Components/Modal',
component: Modal,
argTypes: {
isOpen: { control: 'boolean' },
onClose: { action: 'closed' },
className: { control: 'text' },
overlayClassName: { control: 'text' },
closeButtonClassName: { control: 'text' },
disableEscapeKeyDown: { control: 'boolean' },
'aria-label': { control: 'text' },
'aria-labelledby': { control: 'text' },
'aria-describedby': { control: 'text' },
},
};
export default meta;
type Story = StoryObj<typeof Modal>;
export const Default: Story = {
args: {
isOpen: true,
onClose: () => {
console.log('Modal closed');
},
children: (
<>
<Modal.Header>
<Modal.Title>Modal Title</Modal.Title>
<Modal.Description>
This is a simple modal description.
</Modal.Description>
</Modal.Header>
<Modal.Content>This is the main content of the modal.</Modal.Content>
<Modal.Footer>
<button className="px-4 py-2 bg-blue-500 text-white rounded">
Confirm
</button>
<button className="px-4 py-2 bg-gray-300 rounded">Cancel</button>
</Modal.Footer>
</>
),
},
};
export const CustomClasses: Story = {
args: {
...Default.args,
className: 'bg-red-100',
overlayClassName: 'bg-opacity-50',
closeButtonClassName: 'text-red-500',
},
};
export const NoEscapeClose: Story = {
args: {
...Default.args,
disableEscapeKeyDown: true,
},
};
export const WithAria: Story = {
args: {
...Default.args,
'aria-label': 'Custom Modal',
'aria-labelledby': 'modal-title',
'aria-describedby': 'modal-desc',
children: (
<>
<Modal.Header>
<Modal.Title id="modal-title">Modal Title</Modal.Title>
<Modal.Description id="modal-desc">
This is a modal with ARIA attributes.
</Modal.Description>
</Modal.Header>
<Modal.Content>Modal content here.</Modal.Content>
</>
),
},
};
+190
View File
@@ -0,0 +1,190 @@
import { CloseOutlined } from '@ant-design/icons';
import { cn } from '@imphnen-frontend-service/utils';
import React, { useEffect, useMemo, useCallback } from 'react';
import { createPortal } from 'react-dom';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
className?: string;
overlayClassName?: string;
closeButtonClassName?: string;
disableEscapeKeyDown?: boolean;
'aria-label'?: string;
'aria-labelledby'?: string;
'aria-describedby'?: string;
}
const Modal = ({
isOpen,
onClose,
children,
className,
overlayClassName,
closeButtonClassName,
disableEscapeKeyDown = false,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
'aria-describedby': ariaDescribedBy,
}: ModalProps) => {
const handleEscapeKey = useCallback(
(event: KeyboardEvent) => {
if (event.key === 'Escape' && isOpen && !disableEscapeKeyDown) {
onClose();
}
},
[isOpen, onClose, disableEscapeKeyDown]
);
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
window.addEventListener('keydown', handleEscapeKey);
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
window.removeEventListener('keydown', handleEscapeKey);
};
}, [isOpen, handleEscapeKey]);
const modalNode = useMemo(() => document.createElement('div'), []);
useEffect(() => {
document.body.appendChild(modalNode);
return () => {
document.body.removeChild(modalNode);
};
}, [modalNode]);
if (!isOpen) return null;
return createPortal(
<div className="fixed inset-0 z-50">
<div
className={cn(
'fixed inset-0 bg-gray-900/80 transition-opacity duration-200',
isOpen ? 'opacity-100' : 'opacity-0',
overlayClassName
)}
onClick={onClose}
role="presentation"
/>
<div
className={cn(
'fixed left-[50%] top-[50%] z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 bg-[#F0F8FF] rounded-lg p-6 shadow-xl transition-all duration-200',
'sm:rounded-lg sm:max-w-md',
isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95',
className
)}
role="dialog"
aria-modal="true"
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
aria-describedby={ariaDescribedBy}
>
{children}
<button
onClick={onClose}
className={cn(
'absolute right-4 top-4 rounded-sm p-1 text-gray-500 transition-colors hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-gray-950 focus:ring-offset-2',
closeButtonClassName
)}
aria-label="Close modal"
>
<CloseOutlined className="h-4 w-4 cursor-pointer" />
</button>
</div>
</div>,
modalNode
);
};
interface ModalHeaderProps {
className?: string;
children: React.ReactNode;
}
const ModalHeader = ({ className, children }: ModalHeaderProps) => (
<div
className={cn(
'mb-4 flex flex-col space-y-1.5 text-center sm:text-left',
className
)}
>
{children}
</div>
);
interface ModalContentProps {
className?: string;
children: React.ReactNode;
}
const ModalContent = ({ className, children }: ModalContentProps) => (
<div className={cn('mb-4', className)}>{children}</div>
);
interface ModalFooterProps {
className?: string;
children: React.ReactNode;
}
const ModalFooter = ({ className, children }: ModalFooterProps) => (
<div className={cn('flex gap-2 sm:flex-row sm:justify-end', className)}>
{children}
</div>
);
interface ModalTitleProps {
className?: string;
children: React.ReactNode;
id?: string;
}
const ModalTitle = ({ className, children, id }: ModalTitleProps) => (
<h2
id={id}
className={cn(
'text-lg font-semibold leading-none tracking-tight',
className
)}
>
{children}
</h2>
);
interface ModalDescriptionProps {
className?: string;
children: React.ReactNode;
id?: string;
}
const ModalDescription = ({
className,
children,
id,
}: ModalDescriptionProps) => (
<p id={id} className={cn('text-sm text-gray-500', className)}>
{children}
</p>
);
Modal.Header = ModalHeader;
Modal.Content = ModalContent;
Modal.Footer = ModalFooter;
Modal.Title = ModalTitle;
Modal.Description = ModalDescription;
export { Modal };
export type {
ModalProps,
ModalHeaderProps,
ModalContentProps,
ModalFooterProps,
ModalTitleProps,
ModalDescriptionProps,
};
@@ -0,0 +1 @@
export * from './pagination';
@@ -0,0 +1,23 @@
import type { Meta, StoryObj } from '@storybook/react';
import Pagination from './pagination';
import { PaginationProps } from './index'; // Import PaginationProps
const meta = {
title: 'Molecules/Pagination',
component: Pagination,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof Pagination>;
export default meta;
export const Default: StoryObj<PaginationProps> = {
args: {
currentPage: 1,
totalPages: 5,
onPageChange: (page: number) => console.log('Page changed to:', page),
},
};
@@ -0,0 +1,93 @@
import { Table } from '@tanstack/react-table';
import { ArrowLeftOutlined, ArrowRightOutlined } from '@ant-design/icons';
interface PaginationProps<T> {
table: Table<T>;
}
export const Pagination = <T,>({ table }: PaginationProps<T>) => {
return (
<div className="flex items-center justify-center gap-[40px]">
<button
className="disabled:opacity-50 cursor-pointer"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
aria-label="Previous page"
>
<ArrowLeftOutlined className="text-[16px] text-neutral-800" />
</button>
<div className="flex gap-4 items-baseline">
{table.getPageCount() <= 8 ? (
Array.from({ length: table.getPageCount() }, (_, index) => (
<button
key={index}
className={`size-[30px] py-[8px] flex items-center justify-center rounded-md cursor-pointer ${
table.getState().pagination.pageIndex === index
? 'bg-primary-500 text-white'
: 'bg-primary-100 hover:bg-primary-200'
}`}
onClick={() => table.setPageIndex(index)}
>
{index + 1}
</button>
))
) : (
<>
<button
onClick={() => table.setPageIndex(0)}
className={`size-[30px] py-[8px] flex items-center justify-center rounded-md cursor-pointer ${
table.getState().pagination.pageIndex === 0
? 'bg-primary-500 text-white'
: 'bg-primary-100 hover:bg-primary-200'
}`}
>
1
</button>
{table.getState().pagination.pageIndex > 3 && <span>...</span>}
{Array.from(
{ length: 5 },
(_, index) => table.getState().pagination.pageIndex - 2 + index
)
.filter((page) => page > 0 && page < table.getPageCount() - 1)
.map((page) => (
<button
key={page}
onClick={() => table.setPageIndex(page)}
className={`size-[30px] py-[8px] flex items-center justify-center rounded-md cursor-pointer ${
table.getState().pagination.pageIndex === page
? 'bg-primary-500 text-white'
: 'bg-primary-100 hover:bg-primary-200'
}`}
>
{page + 1}
</button>
))}
{table.getState().pagination.pageIndex <
table.getPageCount() - 4 && <span>...</span>}
<button
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
className={`size-[30px] py-[8px] flex items-center justify-center rounded-md cursor-pointer ${
table.getState().pagination.pageIndex ===
table.getPageCount() - 1
? 'bg-primary-500 text-white'
: 'bg-primary-100 hover:bg-primary-200'
}`}
>
{table.getPageCount()}
</button>
</>
)}
</div>
<button
className="disabled:opacity-50 cursor-pointer"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
aria-label="Next page"
>
<ArrowRightOutlined className="text-[16px] text-neutral-800" />
</button>
</div>
);
};
@@ -0,0 +1,79 @@
import {
AppstoreOutlined,
AuditOutlined,
LogoutOutlined,
UserOutlined,
} from '@ant-design/icons';
import { Button } from '../../atoms';
import { FC, ReactElement } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
export const BackofficeSidebar: FC = (): ReactElement => {
const location = useLocation();
const navigate = useNavigate();
const isActive = (path: string) => location.pathname.includes(path);
return (
<aside className="sticky top-0 left-0 w-[280px] bg-white min-h-screen py-[60px] px-[28px] shadow-xl flex flex-col justify-between">
<div className="flex flex-col gap-20 justify-between items-center">
{/* Logo */}
<img src="/logos/simple.svg" alt="IMPHNEN Logo" className="w-[150px]" />
{/* Navigation Menu */}
<nav className="flex flex-col gap-4 w-full">
<Link
to="/dashboard"
className={`flex items-center justify-items-start gap-3 px-[8px] py-[10px] ${
isActive('/dashboard')
? 'bg-primary-500 text-white rounded-md'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
<AppstoreOutlined className="text-[20px]" />
<span className="text-p3 font-medium">Dashboard & Set Gacha</span>
</Link>
<Link
to="/accounts"
className={`flex items-center justify-items-start gap-3 px-[8px] py-[10px] ${
isActive('/accounts')
? 'bg-primary-500 text-white rounded-md'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
<UserOutlined className="text-[20px]" />
<span className="text-p3 font-medium">Data Akun</span>
</Link>
<Link
to="/transactions"
className={`flex items-center justify-items-start gap-3 px-[8px] py-[10px] ${
isActive('/transactions')
? 'bg-primary-500 text-white rounded-md'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
<AuditOutlined className="text-[20px]" />
<span className="text-p3 font-medium">Validasi Transaksi</span>
</Link>
</nav>
</div>
{/* Log Out Button */}
<div className="w-full">
<hr className="mb-5 border-primary-200" />
<Button
onClick={() => {
navigate('/');
}}
variant="text"
className="items-start justify-start gap-3 px-[8px] py-[10px] text-gray-700 hover:text-red-500 transition-colors w-full"
>
<LogoutOutlined className="text-[20px]" />
<span className="text-p3 font-medium">Log Out</span>
</Button>
</div>
</aside>
);
};
@@ -0,0 +1 @@
export * from './backoffice-sidebar'
@@ -0,0 +1,123 @@
import type { Meta, StoryObj } from '@storybook/react';
import DataTable from './datatable';
const meta = {
title: 'Organisms/DataTable',
component: DataTable,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof DataTable>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
data: [
{
id: 1,
name: 'John Doe',
email: 'john@example.com',
phone: '123-456-7890',
address: '123 Main St',
},
{
id: 2,
name: 'Jane Smith',
email: 'jane@example.com',
phone: '987-654-3210',
address: '456 Elm St',
},
{
id: 3,
name: 'John Smith',
email: 'johns@example.com',
phone: '456-789-1230',
address: '789 Cedar St',
},
],
headers: [
{ label: 'ID', key: 'id' },
{ label: 'Name', key: 'name' },
{ label: 'Email', key: 'email' },
{ label: 'Phone', key: 'phone' },
{ label: 'Address', key: 'address' },
],
onRowClick: (item) => console.log('Row clicked:', item),
},
};
export const WithCustomRender: Story = {
args: {
data: [
{
id: 1,
name: 'John Doe',
email: 'john@example.com',
phone: '123-456-7890',
address: '123 Main St',
status: 'active',
},
{
id: 2,
name: 'Jane Smith',
email: 'jane@example.com',
phone: '987-654-3210',
address: '456 Elm St',
status: 'inactive',
},
{
id: 3,
name: 'John Smith',
email: 'johns@example.com',
phone: '456-789-1230',
address: '789 Cedar St',
status: 'active',
},
],
headers: [
{ label: 'ID', key: 'id' },
{ label: 'Name', key: 'name' },
{ label: 'Email', key: 'email' },
{ label: 'Phone', key: 'phone' },
{
label: 'Status',
render: (item) => (
<span
className={`px-2 py-1 rounded-md text-xs ${
item.status === 'active'
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}
>
{item.status}
</span>
),
},
{
label: 'Actions',
render: (item) => (
<button
className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600"
onClick={(e) => {
e.stopPropagation();
console.log('Edit item with id:', item.id);
}}
>
Edit
</button>
),
},
],
onRowClick: (item) => console.log('Row clicked:', item),
},
};
export const WithoutCheckbox: Story = {
args: {
...Default.args,
showCheckbox: false,
},
};
@@ -0,0 +1,84 @@
import {
PaginationState,
useReactTable,
getCoreRowModel,
getPaginationRowModel,
flexRender,
ColumnDef,
} from '@tanstack/react-table';
import { Pagination } from '../../molecules';
import React from 'react';
interface DataTableProps<T> {
data: T[];
columns: ColumnDef<T>[];
pageSize?: number;
}
export const DataTable = <T,>({
data,
columns,
pageSize = 9,
}: DataTableProps<T>) => {
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize,
});
const table = useReactTable({
data,
columns,
state: {
pagination,
},
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
});
return (
<div className="flex flex-col gap-8">
<div className="w-full overflow-x-auto">
<table className="w-full min-w-full text-base">
<thead className="bg-primary-50 mb-3 text-left">
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th
key={header.id}
className="py-3 px-5 font-normal first:rounded-l-lg last:rounded-r-lg"
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
<tbody className="mt-3">
{table.getRowModel().rows.map((row) => (
<tr key={row.id} className="bg-primary-100 odd:bg-white">
{row.getVisibleCells().map((cell, index) => (
<td
key={cell.id}
className="py-3 px-5 first:rounded-l-lg last:rounded-r-lg"
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<Pagination table={table} />
</div>
);
};
export default DataTable;

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