diff --git a/apps/backoffice/public/gacha-clip.png b/apps/backoffice/public/gacha-clip.png
new file mode 100644
index 0000000..9fe05c0
Binary files /dev/null and b/apps/backoffice/public/gacha-clip.png differ
diff --git a/apps/backoffice/public/gacha.png b/apps/backoffice/public/gacha.png
new file mode 100644
index 0000000..04a57c3
Binary files /dev/null and b/apps/backoffice/public/gacha.png differ
diff --git a/apps/backoffice/public/logos/logo.svg b/apps/backoffice/public/logos/logo.svg
new file mode 100644
index 0000000..ca0f36e
--- /dev/null
+++ b/apps/backoffice/public/logos/logo.svg
@@ -0,0 +1,9 @@
+
diff --git a/apps/backoffice/public/logos/simple.svg b/apps/backoffice/public/logos/simple.svg
new file mode 100644
index 0000000..0fd70f3
--- /dev/null
+++ b/apps/backoffice/public/logos/simple.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/apps/backoffice/src/app/accounts/layout.tsx b/apps/backoffice/src/app/accounts/layout.tsx
new file mode 100644
index 0000000..5d2c643
--- /dev/null
+++ b/apps/backoffice/src/app/accounts/layout.tsx
@@ -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 (
+
+ );
+};
+
+export default AppLayout;
diff --git a/apps/backoffice/src/app/accounts/page.tsx b/apps/backoffice/src/app/accounts/page.tsx
new file mode 100644
index 0000000..5191e88
--- /dev/null
+++ b/apps/backoffice/src/app/accounts/page.tsx
@@ -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[] = [
+ {
+ id: 'select',
+ header: ({ table }) => (
+
+ ),
+ cell: ({ row }) => (
+
+ ),
+ },
+ {
+ 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 }) => (
+
+ ),
+ },
+];
+
+export const Components: FC = (): ReactElement => {
+ const [pagination, setPagination] = React.useState({
+ pageIndex: 0,
+ pageSize: 9,
+ });
+
+ const [rowSelection, setRowSelection] = React.useState({});
+
+ 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 (
+
+ {/* Header */}
+
+
+ {/* Account Table Section */}
+
+ {/* Search and Filter */}
+
+
+ {/* Table */}
+
+
+
+ );
+};
+
+export default Components;
diff --git a/apps/backoffice/src/app/dashboard/layout.tsx b/apps/backoffice/src/app/dashboard/layout.tsx
new file mode 100644
index 0000000..5d2c643
--- /dev/null
+++ b/apps/backoffice/src/app/dashboard/layout.tsx
@@ -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 (
+
+ );
+};
+
+export default AppLayout;
diff --git a/apps/backoffice/src/app/dashboard/page.tsx b/apps/backoffice/src/app/dashboard/page.tsx
new file mode 100644
index 0000000..d1f76c6
--- /dev/null
+++ b/apps/backoffice/src/app/dashboard/page.tsx
@@ -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 (
+
+ {/* Dashboard Header */}
+
+
+
+
+ {/* Summary Section */}
+
+
+ Summary
+
+
+ {/* Summary Cards */}
+ {[1, 2, 3, 4].map((item) => (
+
+
+
+
1000
+
+ Total Participants
+
+
+
+ ))}
+
+
+
+ {/* Gacha Items Section */}
+
+
+
+ Gacha Items
+
+
+
+
+ {/* Gacha Items List */}
+
+ {[1, 2, 3, 4, 5, 6].map((item) => (
+
+
+
+
+ Lanyard IMPHNEN
+
+
+ Prize {item}
+ Chance Rate: (0.1%)
+
+
+
+
+
+
+
+
+ {/* Lebih baik gunakan gambar yang sudah di-clip dengan size height: 78px daripada hard-code object-position dan margin */}
+

+
+ ))}
+
+
+
+
+ {/* Right-side illustration */}
+

+
+
+ );
+};
+
+export default Components;
diff --git a/apps/backoffice/src/app/layout.tsx b/apps/backoffice/src/app/layout.tsx
index 7428943..2d8a74d 100644
--- a/apps/backoffice/src/app/layout.tsx
+++ b/apps/backoffice/src/app/layout.tsx
@@ -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 (
-
);
diff --git a/apps/backoffice/src/app/page.tsx b/apps/backoffice/src/app/page.tsx
index f135208..b92bf58 100644
--- a/apps/backoffice/src/app/page.tsx
+++ b/apps/backoffice/src/app/page.tsx
@@ -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 (
+
+
+

+
+ Welcome to IMPHNEN Backoffice
+
+
+
+
+
+
+ );
};
export default Components;
diff --git a/apps/backoffice/src/app/transactions/layout.tsx b/apps/backoffice/src/app/transactions/layout.tsx
new file mode 100644
index 0000000..5d2c643
--- /dev/null
+++ b/apps/backoffice/src/app/transactions/layout.tsx
@@ -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 (
+
+ );
+};
+
+export default AppLayout;
diff --git a/apps/backoffice/src/app/transactions/page.tsx b/apps/backoffice/src/app/transactions/page.tsx
new file mode 100644
index 0000000..fb8428f
--- /dev/null
+++ b/apps/backoffice/src/app/transactions/page.tsx
@@ -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[] = [
+ {
+ id: 'select',
+ header: ({ table }) => (
+
+ ),
+ cell: ({ row }) => (
+
+ ),
+ },
+ {
+ 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 = {
+ valid: 'bg-success-500 text-white',
+ invalid: 'bg-danger-500 text-white',
+ unchecked: 'bg-yellow-400 text-black',
+ };
+ const statusText: Record = {
+ valid: 'Valid',
+ invalid: 'Invalid',
+ unchecked: 'Unchecked',
+ };
+ return (
+
+ {statusText[status]}
+
+ );
+ },
+ },
+ {
+ header: 'Action',
+ cell: ({ row }) => (
+
+ ),
+ },
+];
+
+export const Components: FC = (): ReactElement => {
+ const [pagination, setPagination] = React.useState({
+ pageIndex: 0,
+ pageSize: 9,
+ });
+
+ const [rowSelection, setRowSelection] = React.useState({});
+
+ 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 (
+
+ {/* Header */}
+
+
+ {/* Account Table Section */}
+
+ {/* Search and Filter */}
+
+
+ {/* Table */}
+
+
+
+ );
+};
+
+export default Components;
diff --git a/apps/backoffice/src/index.css b/apps/backoffice/src/index.css
index e9bf12a..b92b6ea 100644
--- a/apps/backoffice/src/index.css
+++ b/apps/backoffice/src/index.css
@@ -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;
@@ -137,4 +133,4 @@
font-size: 12px;
line-height: 1.2;
}
-}
+}
\ No newline at end of file
diff --git a/apps/gacha/src/index.css b/apps/gacha/src/index.css
index e9bf12a..b92b6ea 100644
--- a/apps/gacha/src/index.css
+++ b/apps/gacha/src/index.css
@@ -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;
@@ -137,4 +133,4 @@
font-size: 12px;
line-height: 1.2;
}
-}
+}
\ No newline at end of file
diff --git a/libs/ui/src/atoms/index.ts b/libs/ui/src/atoms/index.ts
index c107e65..dc8c232 100644
--- a/libs/ui/src/atoms/index.ts
+++ b/libs/ui/src/atoms/index.ts
@@ -1,4 +1,3 @@
export * from './button';
export * from './input';
-export * from './password-input';
export * from './textarea';
diff --git a/libs/ui/src/atoms/input/input.spec.tsx b/libs/ui/src/atoms/input/input.spec.tsx
index 4fdf16b..a0d57cc 100644
--- a/libs/ui/src/atoms/input/input.spec.tsx
+++ b/libs/ui/src/atoms/input/input.spec.tsx
@@ -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();
- 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();
- // expect(screen.getByRole("textbox")).toHaveClass(className);
- // });
- // });
-
- // it("renders the input with different types", () => {
- // const types = ["text", "password", "email", "number"];
- // types.forEach((type) => {
- // render();
- // const input = screen.getByRole("textbox");
- // expect(input).toHaveAttribute("type", type);
- // });
- // });
-
it("disables the input field when 'disabled' prop is set", () => {
render();
- expect(screen.getByRole("textbox")).toBeDisabled();
+ expect(screen.getByRole('textbox')).toBeDisabled();
});
});
diff --git a/libs/ui/src/atoms/input/input.stories.tsx b/libs/ui/src/atoms/input/input.stories.tsx
index 7bea8f5..371aaa7 100644
--- a/libs/ui/src/atoms/input/input.stories.tsx
+++ b/libs/ui/src/atoms/input/input.stories.tsx
@@ -4,17 +4,8 @@ import { Input } from './input';
const meta: Meta = {
title: 'Components/Input',
component: Input,
- argTypes: {
- type: {
- control: 'select',
- options: ['text', 'email'],
- },
- size: {
- control: 'select',
- options: ['sm', 'md', 'lg'],
- },
- },
-};
+ tags: ['autodocs'],
+} satisfies Meta;
export default meta;
type Story = StoryObj;
@@ -22,6 +13,7 @@ type Story = StoryObj;
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',
- },
-};
diff --git a/libs/ui/src/atoms/input/input.tsx b/libs/ui/src/atoms/input/input.tsx
index 3a80d49..a20ed2d 100644
--- a/libs/ui/src/atoms/input/input.tsx
+++ b/libs/ui/src/atoms/input/input.tsx
@@ -3,10 +3,13 @@ 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';
+type TInputType = 'text' | 'email' | 'password';
type TInputSize = 'sm' | 'md' | 'lg';
type TInputProps = Omit<
@@ -15,46 +18,70 @@ type TInputProps = Omit<
> & {
type?: TInputType;
size?: TInputSize;
- error?: string;
+ disabled?: boolean;
};
-const sizeClasses: Record = {
- sm: 'text-[10px] max-h-[28px]',
- md: 'text-[12px] max-h-[30px]',
- lg: 'text-[15px] max-h-[34px]',
-};
+const sizeClasses: Record =
+ {
+ 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 = ({
+ 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 (
- <>
+
- {error &&
{error}
}
- >
+ {type === 'password' && (
+
+
+
+ )}
+
);
};
diff --git a/libs/ui/src/atoms/password-input/index.ts b/libs/ui/src/atoms/password-input/index.ts
deleted file mode 100644
index aa25c3b..0000000
--- a/libs/ui/src/atoms/password-input/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from "./password-input";
\ No newline at end of file
diff --git a/libs/ui/src/atoms/password-input/password-input.stories.tsx b/libs/ui/src/atoms/password-input/password-input.stories.tsx
deleted file mode 100644
index d2db711..0000000
--- a/libs/ui/src/atoms/password-input/password-input.stories.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { Meta, StoryObj } from '@storybook/react';
-import { PasswordInput } from './password-input';
-
-const meta: Meta = {
- title: 'Components/Password Input',
- component: PasswordInput,
- argTypes: {
- size: {
- control: 'select',
- options: ['sm', 'md', 'lg'],
- },
- },
-};
-
-export default meta;
-type Story = StoryObj;
-
-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',
- },
-};
diff --git a/libs/ui/src/atoms/password-input/password-input.tsx b/libs/ui/src/atoms/password-input/password-input.tsx
deleted file mode 100644
index 85372fa..0000000
--- a/libs/ui/src/atoms/password-input/password-input.tsx
+++ /dev/null
@@ -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, HTMLInputElement>,
- 'size' | 'type'
-> & {
- type?: TPasswordInputType;
- size?: TPasswordInputSize;
- error?: string;
-};
-
-const sizeClasses: Record = {
- sm: 'text-[10px] max-h-[28px]',
- md: 'text-[12px] max-h-[30px]',
- lg: 'text-[15px] max-h-[34px]',
-};
-
-const iconSizeClasses: Record = {
- 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 = ({
- 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 (
- <>
-
-
-
-
-
-
- {error && {error}
}
- >
- );
-};
diff --git a/libs/ui/src/index.css b/libs/ui/src/index.css
index 6d48aaa..b92b6ea 100644
--- a/libs/ui/src/index.css
+++ b/libs/ui/src/index.css
@@ -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;
}
-}
+}
\ No newline at end of file
diff --git a/libs/ui/src/molecules/index.ts b/libs/ui/src/molecules/index.ts
index 020d5fe..cda9a98 100644
--- a/libs/ui/src/molecules/index.ts
+++ b/libs/ui/src/molecules/index.ts
@@ -1 +1,3 @@
-export * from "./forgot-step";
\ No newline at end of file
+export * from "./forgot-step";
+export * from './input-form';
+export * from './pagination';
diff --git a/libs/ui/src/molecules/input-form/index.ts b/libs/ui/src/molecules/input-form/index.ts
new file mode 100644
index 0000000..14dcfcb
--- /dev/null
+++ b/libs/ui/src/molecules/input-form/index.ts
@@ -0,0 +1 @@
+export * from './input-form';
diff --git a/libs/ui/src/molecules/input-form/input-form.spec.tsx b/libs/ui/src/molecules/input-form/input-form.spec.tsx
new file mode 100644
index 0000000..f0dc650
--- /dev/null
+++ b/libs/ui/src/molecules/input-form/input-form.spec.tsx
@@ -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();
+
+ const input = screen.getByLabelText('Test Label');
+ expect(input).toBeDisabled();
+ expect(input).toHaveClass('opacity-50 cursor-not-allowed');
+ });
+
+ it('renders correctly without disabled prop', () => {
+ render();
+
+ const input = screen.getByLabelText('Test Label');
+ expect(input).not.toBeDisabled();
+ expect(input).not.toHaveClass('opacity-50 cursor-not-allowed');
+ });
+});
diff --git a/libs/ui/src/molecules/input-form/input-form.stories.tsx b/libs/ui/src/molecules/input-form/input-form.stories.tsx
new file mode 100644
index 0000000..024805a
--- /dev/null
+++ b/libs/ui/src/molecules/input-form/input-form.stories.tsx
@@ -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;
+
+export default meta;
+type Story = StoryObj;
+
+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,
+ },
+};
diff --git a/libs/ui/src/molecules/input-form/input-form.tsx b/libs/ui/src/molecules/input-form/input-form.tsx
new file mode 100644
index 0000000..1af658d
--- /dev/null
+++ b/libs/ui/src/molecules/input-form/input-form.tsx
@@ -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, HTMLInputElement>,
+ 'size' | 'type'
+> & {
+ label: string;
+ type?: TInputType;
+ size?: TInputSize;
+ error?: string;
+ disabled?: boolean;
+
+ helperText?: string;
+ htmlFor?: string;
+};
+
+const sizeClasses: Record = {
+ 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 = ({
+ label,
+ placeholder,
+ type = 'text',
+ size = 'md',
+ error,
+ helperText,
+ htmlFor,
+ className,
+ disabled,
+ ...rest
+}): ReactElement => {
+ return (
+
+
+
+ {error ? (
+
{error}
+ ) : (
+ helperText && (
+
+ {helperText}
+
+ )
+ )}
+
+ );
+};
+
+export default InputForm;
diff --git a/libs/ui/src/molecules/pagination/index.ts b/libs/ui/src/molecules/pagination/index.ts
new file mode 100644
index 0000000..cb72765
--- /dev/null
+++ b/libs/ui/src/molecules/pagination/index.ts
@@ -0,0 +1 @@
+export * from './pagination';
diff --git a/libs/ui/src/molecules/pagination/pagination.stories.tsx b/libs/ui/src/molecules/pagination/pagination.stories.tsx
new file mode 100644
index 0000000..43b1888
--- /dev/null
+++ b/libs/ui/src/molecules/pagination/pagination.stories.tsx
@@ -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;
+
+export default meta;
+
+export const Default: StoryObj = {
+ args: {
+ currentPage: 1,
+ totalPages: 5,
+ onPageChange: (page: number) => console.log('Page changed to:', page),
+ },
+};
diff --git a/libs/ui/src/molecules/pagination/pagination.tsx b/libs/ui/src/molecules/pagination/pagination.tsx
new file mode 100644
index 0000000..7518854
--- /dev/null
+++ b/libs/ui/src/molecules/pagination/pagination.tsx
@@ -0,0 +1,93 @@
+import { Table } from '@tanstack/react-table';
+import { ArrowLeftOutlined, ArrowRightOutlined } from '@ant-design/icons';
+
+interface PaginationProps {
+ table: Table;
+}
+
+export const Pagination = ({ table }: PaginationProps) => {
+ return (
+
+
+
+
+ {table.getPageCount() <= 8 ? (
+ Array.from({ length: table.getPageCount() }, (_, index) => (
+
+ ))
+ ) : (
+ <>
+
+ {table.getState().pagination.pageIndex > 3 && ...}
+ {Array.from(
+ { length: 5 },
+ (_, index) => table.getState().pagination.pageIndex - 2 + index
+ )
+ .filter((page) => page > 0 && page < table.getPageCount() - 1)
+ .map((page) => (
+
+ ))}
+ {table.getState().pagination.pageIndex <
+ table.getPageCount() - 4 && ...}
+
+ >
+ )}
+
+
+
+
+ );
+};
diff --git a/libs/ui/src/organisms/backoffice-sidebar/backoffice-sidebar.tsx b/libs/ui/src/organisms/backoffice-sidebar/backoffice-sidebar.tsx
new file mode 100644
index 0000000..b1aa794
--- /dev/null
+++ b/libs/ui/src/organisms/backoffice-sidebar/backoffice-sidebar.tsx
@@ -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 (
+
+ );
+};
diff --git a/libs/ui/src/organisms/backoffice-sidebar/index.ts b/libs/ui/src/organisms/backoffice-sidebar/index.ts
new file mode 100644
index 0000000..cc302f9
--- /dev/null
+++ b/libs/ui/src/organisms/backoffice-sidebar/index.ts
@@ -0,0 +1 @@
+export * from './backoffice-sidebar'
diff --git a/libs/ui/src/organisms/datatable/datatable.stories.tsx b/libs/ui/src/organisms/datatable/datatable.stories.tsx
new file mode 100644
index 0000000..aa80961
--- /dev/null
+++ b/libs/ui/src/organisms/datatable/datatable.stories.tsx
@@ -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;
+
+export default meta;
+type Story = StoryObj;
+
+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) => (
+
+ {item.status}
+
+ ),
+ },
+ {
+ label: 'Actions',
+ render: (item) => (
+
+ ),
+ },
+ ],
+ onRowClick: (item) => console.log('Row clicked:', item),
+ },
+};
+
+export const WithoutCheckbox: Story = {
+ args: {
+ ...Default.args,
+ showCheckbox: false,
+ },
+};
diff --git a/libs/ui/src/organisms/datatable/datatable.tsx b/libs/ui/src/organisms/datatable/datatable.tsx
new file mode 100644
index 0000000..dddbcf5
--- /dev/null
+++ b/libs/ui/src/organisms/datatable/datatable.tsx
@@ -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 {
+ data: T[];
+ columns: ColumnDef[];
+ pageSize?: number;
+}
+
+export const DataTable = ({
+ data,
+ columns,
+ pageSize = 9,
+}: DataTableProps) => {
+ const [pagination, setPagination] = React.useState({
+ pageIndex: 0,
+ pageSize,
+ });
+
+ const table = useReactTable({
+ data,
+ columns,
+ state: {
+ pagination,
+ },
+ getCoreRowModel: getCoreRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ onPaginationChange: setPagination,
+ });
+
+ return (
+
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => (
+ |
+ {header.isPlaceholder
+ ? null
+ : flexRender(
+ header.column.columnDef.header,
+ header.getContext()
+ )}
+ |
+ ))}
+
+ ))}
+
+
+ {table.getRowModel().rows.map((row) => (
+
+ {row.getVisibleCells().map((cell, index) => (
+ |
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+ |
+ ))}
+
+ ))}
+
+
+
+
+
+ );
+};
+
+export default DataTable;
diff --git a/libs/ui/src/organisms/datatable/index.ts b/libs/ui/src/organisms/datatable/index.ts
new file mode 100644
index 0000000..1c33f72
--- /dev/null
+++ b/libs/ui/src/organisms/datatable/index.ts
@@ -0,0 +1 @@
+export * from './datatable';
diff --git a/libs/ui/src/organisms/index.ts b/libs/ui/src/organisms/index.ts
index 45dd25a..b94d45a 100644
--- a/libs/ui/src/organisms/index.ts
+++ b/libs/ui/src/organisms/index.ts
@@ -1,3 +1,5 @@
export * from './navbar';
export * from "./modals-gacha";
-export * from "./auth-banner";
\ No newline at end of file
+export * from "./auth-banner";
+export * from './backoffice-sidebar'
+export * from './datatable'
diff --git a/libs/ui/src/organisms/modals-gacha/modals-gacha.spec.tsx b/libs/ui/src/organisms/modals-gacha/modals-gacha.spec.tsx
deleted file mode 100644
index e69de29..0000000
diff --git a/package-lock.json b/package-lock.json
index 0379858..c4d353c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 4c65385..4a3cf8a 100644
--- a/package.json
+++ b/package.json
@@ -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",