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 */} +
+

Data Akun

+
+ + {/* 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 */} +
+

Dashboard

+
+ +
+
+ {/* 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 */} + Lanyard IMPHNEN +
+ ))} +
+
+
+ + {/* 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 */} +
+

Validasi Transaksi

+
+ + {/* 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 5ac3047..a20ed2d 100644 --- a/libs/ui/src/atoms/input/input.tsx +++ b/libs/ui/src/atoms/input/input.tsx @@ -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 = { - 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 0f296dd..2dd96d6 100644 --- a/libs/ui/src/molecules/index.ts +++ b/libs/ui/src/molecules/index.ts @@ -1,2 +1,4 @@ export * from "./forgot-step"; -export * from "./otp-form"; \ No newline at end of file +export * from "./otp-form"; +export * from './input-form'; +export * from './pagination'; \ No newline at end of file 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) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell, index) => ( + + ))} + + ))} + +
+ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} +
+ {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",