fix: conflict

This commit is contained in:
boboiazumi
2025-03-29 20:47:38 +07:00
39 changed files with 1450 additions and 379 deletions
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;
-2
View File
@@ -1,11 +1,9 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
import { Navbar } from '@imphnen-frontend-service/ui/organisms';
export const AppLayout: FC = (): ReactElement => {
return (
<main className="bg-primary-50 min-h-screen">
<Navbar />
<Outlet />
</main>
);
+30 -1
View File
@@ -1,7 +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 => {
return <>Hallo</>;
const navigate = useNavigate();
return (
<div className="flex justify-center items-center min-h-screen">
<div className="bg-white border border-primary-200 shadow-lg p-[60px] text-center flex flex-col justify-items-stretch gap-8 rounded-2xl">
<img src="/logos/logo.svg" alt="" className="h-[70px] w-auto" />
<h1 className="text-primary-500 text-p1 font-semibold">
Welcome to IMPHNEN Backoffice
</h1>
<InputForm
label="Email"
placeholder="Masukkan Email"
type="email"
size="lg"
className="w-full"
/>
<InputForm
label="Password"
placeholder="Masukkan password"
type="password"
size="lg"
className="w-full"
/>
<Button onClick={() => navigate('/dashboard')}>Login</Button>
</div>
</div>
);
};
export default Components;
@@ -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;
+36 -40
View File
@@ -27,49 +27,45 @@
--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;
+36 -40
View File
@@ -27,49 +27,45 @@
--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;
-1
View File
@@ -1,4 +1,3 @@
export * from './button';
export * from './input';
export * from './password-input';
export * from './textarea';
+7 -28
View File
@@ -1,36 +1,15 @@
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { Input } from "./input";
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", () => {
describe('Test Input Component', () => {
it('renders the input with placeholder text', () => {
render(<Input placeholder="Placeholder" />);
expect(screen.getByPlaceholderText("Placeholder")).toBeInTheDocument();
expect(screen.getByPlaceholderText('Placeholder')).toBeInTheDocument();
});
// it("renders the input with different sizes", () => {
// const sizeClasses = {
// sm: "text-[10px] max-h-[28px]",
// md: "text-[12px] max-h-[30px]",
// lg: "text-[15px] max-h-[34px]",
// };
// Object.entries(sizeClasses).forEach(([size, className]) => {
// render(<Input size={size} />);
// expect(screen.getByRole("textbox")).toHaveClass(className);
// });
// });
// it("renders the input with different types", () => {
// const types = ["text", "password", "email", "number"];
// types.forEach((type) => {
// render(<Input type={type} />);
// const input = screen.getByRole("textbox");
// expect(input).toHaveAttribute("type", type);
// });
// });
it("disables the input field when 'disabled' prop is set", () => {
render(<Input disabled />);
expect(screen.getByRole("textbox")).toBeDisabled();
expect(screen.getByRole('textbox')).toBeDisabled();
});
});
+12 -19
View File
@@ -4,17 +4,8 @@ import { Input } from './input';
const meta: Meta<typeof Input> = {
title: 'Components/Input',
component: Input,
argTypes: {
type: {
control: 'select',
options: ['text', 'email'],
},
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
},
},
};
tags: ['autodocs'],
} satisfies Meta<typeof Input>;
export default meta;
type Story = StoryObj<typeof Input>;
@@ -22,6 +13,7 @@ type Story = StoryObj<typeof Input>;
export const Large: Story = {
args: {
size: 'lg',
type: 'text',
placeholder: 'Placeholder',
},
};
@@ -29,6 +21,7 @@ export const Large: Story = {
export const Medium: Story = {
args: {
size: 'md',
type: 'text',
placeholder: 'Placeholder',
},
};
@@ -36,6 +29,7 @@ export const Medium: Story = {
export const Small: Story = {
args: {
size: 'sm',
type: 'text',
placeholder: 'Placeholder',
},
};
@@ -54,6 +48,13 @@ export const EmailInput: Story = {
},
};
export const PasswordInput: Story = {
args: {
size: 'md',
type: 'password',
},
};
export const Disabled: Story = {
args: {
size: 'md',
@@ -61,11 +62,3 @@ export const Disabled: Story = {
disabled: true,
},
};
export const WithError: Story = {
args: {
size: 'md',
type: 'text',
error: 'This field is required',
},
};
+44 -17
View File
@@ -3,8 +3,11 @@ import {
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';
@@ -15,46 +18,70 @@ type TInputProps = Omit<
> & {
type?: TInputType;
size?: TInputSize;
error?: string;
disabled?: boolean;
};
const sizeClasses: Record<TInputSize, string> = {
sm: 'text-[10px] max-h-[28px]',
md: 'text-[12px] max-h-[30px]',
lg: 'text-[15px] max-h-[34px]',
};
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';
const errorClass =
'border-danger-500 hover:border-danger-500 focus:outline-danger-500';
export const Input: FC<TInputProps> = ({
type = 'text',
size = 'md',
type,
placeholder = 'Placeholder',
disabled,
error,
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',
sizeClasses[size],
'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,
error && errorClass,
className
);
return (
<>
<div className="relative flex items-center">
<input
className={mergedClassName}
type={type}
type={type === 'password' && showPassword ? 'text' : type}
disabled={disabled}
placeholder={placeholder}
{...rest}
/>
{error && <p className="text-danger-500 text-xs mt-1">{error}</p>}
</>
{type === 'password' && (
<div className="absolute end-0 px-[12px] h-full flex items-center">
<Button
variant="text"
size={size}
onClick={togglePasswordVisibility}
className={cn(
'relative aspect-square -me-[8px] p-[6px]',
sizeClasses[size].iconSize,
disabled && 'cursor-not-allowed'
)}
>
{showPassword ? (
<EyeInvisibleOutlined
style={{ color: 'var(--color-neutral-500)' }}
/>
) : (
<EyeOutlined style={{ color: 'var(--color-neutral-500)' }} />
)}
</Button>
</div>
)}
</div>
);
};
@@ -1 +0,0 @@
export * from "./password-input";
@@ -1,58 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { PasswordInput } from './password-input';
const meta: Meta<typeof PasswordInput> = {
title: 'Components/Password Input',
component: PasswordInput,
argTypes: {
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
},
},
};
export default meta;
type Story = StoryObj<typeof PasswordInput>;
export const Large: Story = {
args: {
size: 'lg',
placeholder: 'Placeholder',
},
};
export const Medium: Story = {
args: {
size: 'md',
placeholder: 'Placeholder',
},
};
export const Small: Story = {
args: {
size: 'sm',
placeholder: 'Placeholder',
},
};
export const Input: Story = {
args: {
size: 'md',
placeholder: 'Placeholder',
},
};
export const Disabled: Story = {
args: {
size: 'md',
disabled: true,
},
};
export const WithError: Story = {
args: {
size: 'md',
error: 'This field is required',
},
};
@@ -1,111 +0,0 @@
import {
DetailedHTMLProps,
FC,
InputHTMLAttributes,
ReactElement,
useState,
} from 'react';
import { cn } from '@imphnen-frontend-service/utils';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons';
type TPasswordInputType = 'password';
type TPasswordInputSize = 'sm' | 'md' | 'lg';
type TPasswordInputProps = Omit<
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
'size' | 'type'
> & {
type?: TPasswordInputType;
size?: TPasswordInputSize;
error?: string;
};
const sizeClasses: Record<TPasswordInputSize, string> = {
sm: 'text-[10px] max-h-[28px]',
md: 'text-[12px] max-h-[30px]',
lg: 'text-[15px] max-h-[34px]',
};
const iconSizeClasses: Record<TPasswordInputSize, string> = {
sm: 'text-[10px]',
md: 'text-[12px]',
lg: 'text-[16px]',
};
const disabledClass = 'opacity-50 hover:border-neutral-200 cursor-not-allowed';
const errorClass =
'border-danger-500 hover:border-danger-500 focus:outline-danger-500';
export const PasswordInput: FC<TPasswordInputProps> = ({
size = 'md',
placeholder = 'Placeholder',
disabled,
error,
className,
...rest
}): ReactElement => {
const [showPassword, setShowPassword] = useState(false);
const togglePasswordVisibility = () => {
if (!disabled) setShowPassword((prev) => !prev);
};
const mergedClassName = cn(
'px-[12px] py-[8px] text-neutral-800 placeholder:text-neutral-300 border border-neutral-200 hover:border-blue-300 focus:outline-1 focus:outline-blue-500 rounded-md font-bai-jamjuree',
sizeClasses[size],
disabled && disabledClass,
error && errorClass,
className
);
return (
<>
<div
className={cn(
'relative inline-flex items-center border border-neutral-200 rounded-md',
mergedClassName
)}
>
<input
className="focus:outline-0"
type={showPassword ? 'text' : 'password'}
disabled={disabled}
placeholder={placeholder}
{...rest}
/>
<div className="absolute end-0 px-[12px] h-full flex items-center">
<Button
variant="text"
size={size}
onClick={togglePasswordVisibility}
className={cn(
'relative aspect-square -me-[10px] p-[6px]',
iconSizeClasses[size],
disabled && 'cursor-not-allowed'
)}
>
{showPassword ? (
<EyeInvisibleOutlined
style={{
color: error
? 'var(--color-danger-500)'
: 'var(--color-neutral-500)',
}}
/>
) : (
<EyeOutlined
style={{
color: error
? 'var(--color-danger-500)'
: 'var(--color-neutral-500)',
}}
/>
)}
</Button>
</div>
</div>
{error && <p className="text-danger-500 text-xs mt-1">{error}</p>}
</>
);
};
+72 -56
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;
}
}
+2
View File
@@ -1,2 +1,4 @@
export * from "./forgot-step";
export * from "./otp-form";
export * from './input-form';
export * from './pagination';
@@ -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;
@@ -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;
+1
View File
@@ -0,0 +1 @@
export * from './datatable';
+2
View File
@@ -1,3 +1,5 @@
export * from './navbar';
export * from "./modals-gacha";
export * from "./auth-banner";
export * from './backoffice-sidebar'
export * from './datatable'
+34
View File
@@ -11,6 +11,7 @@
"dependencies": {
"@ant-design/icons": "^5.6.1",
"@tanstack/react-query": "^5.67.3",
"@tanstack/react-table": "^8.21.2",
"axios": "^1.8.3",
"clsx": "^2.1.1",
"react": "^19.0.0",
@@ -8817,6 +8818,39 @@
"react": "^18 || ^19"
}
},
"node_modules/@tanstack/react-table": {
"version": "8.21.2",
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.2.tgz",
"integrity": "sha512-11tNlEDTdIhMJba2RBH+ecJ9l1zgS2kjmexDPAraulc8jeNA4xocSNeyzextT0XJyASil4XsCYlJmf5jEWAtYg==",
"license": "MIT",
"dependencies": {
"@tanstack/table-core": "8.21.2"
},
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/@tanstack/table-core": {
"version": "8.21.2",
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.2.tgz",
"integrity": "sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@testing-library/dom": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
+1
View File
@@ -20,6 +20,7 @@
"dependencies": {
"@ant-design/icons": "^5.6.1",
"@tanstack/react-query": "^5.67.3",
"@tanstack/react-table": "^8.21.2",
"axios": "^1.8.3",
"clsx": "^2.1.1",
"react": "^19.0.0",