This commit is contained in:
David Arranz 2024-06-11 18:48:09 +02:00
parent 090f8acf5c
commit e6498b4104
59 changed files with 3506 additions and 704 deletions

13
client/jest.config.ts Normal file
View File

@ -0,0 +1,13 @@
// jest.config.ts
export default {
preset: "ts-jest",
testEnvironment: "jest-environment-jsdom",
transform: {
"^.+\\.tsx?$": "ts-jest",
// process `*.tsx` files with `ts-jest`
},
moduleNameMapper: {
"\\.(gif|ttf|eot|svg|png)$": "<rootDir>/test/__ mocks __/fileMock.js",
},
};

View File

@ -8,9 +8,12 @@
"dev": "vite --host",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
"preview": "vite preview",
"test": "jest"
},
"dependencies": {
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@hookform/resolvers": "^3.5.0",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
@ -40,6 +43,7 @@
"@radix-ui/react-toggle-group": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "^5.39.0",
"@tanstack/react-table": "^8.17.3",
"axios": "^1.7.2",
"class-variance-authority": "^0.7.0",
"cmdk": "^1.0.0",
@ -62,6 +66,10 @@
},
"devDependencies": {
"@tanstack/react-query-devtools": "^5.39.0",
"@testing-library/jest-dom": "^6.4.6",
"@testing-library/react": "^16.0.0",
"@testing-library/react-hooks": "^8.0.1",
"@types/jest": "^29.5.12",
"@types/node": "^20.14.0",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
@ -73,10 +81,15 @@
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"postcss": "^8.4.38",
"postcss-import": "^16.1.0",
"tailwind-merge": "^2.3.0",
"tailwindcss": "^3.4.3",
"ts-jest": "^29.1.4",
"ts-node": "^10.9.2",
"typescript": "^5.2.2",
"vite": "^5.2.12"
}

View File

@ -1,8 +1,7 @@
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import { LoginPage, LogoutPage, SettingsPage, StartPage } from "./app";
import { CatalogList } from "./app/catalog";
import { Outlet, RouterProvider, createBrowserRouter } from "react-router-dom";
import { DealerLayout, DealersList, LoginPage, LogoutPage, SettingsPage, StartPage } from "./app";
import { CatalogLayout, CatalogList } from "./app/catalog";
import { DashboardPage } from "./app/dashboard";
import { DealersList } from "./app/dealers/list";
import { QuotesList } from "./app/quotes/list";
import { ProtectedRoute } from "./components";
@ -29,17 +28,33 @@ export const Routes = () => {
path: "/catalog",
element: (
<ProtectedRoute>
<CatalogList />
<CatalogLayout>
<Outlet />
</CatalogLayout>
</ProtectedRoute>
),
children: [
{
index: true,
element: <CatalogList />,
},
],
},
{
path: "/dealers",
element: (
<ProtectedRoute>
<DealersList />
<DealerLayout>
<Outlet />
</DealerLayout>
</ProtectedRoute>
),
children: [
{
index: true,
element: <DealersList />,
},
],
},
{
path: "/quotes",

View File

@ -0,0 +1,54 @@
import { IDataSource } from '@/lib/hooks/useDataSource/DataSource';
export type SuccessNotificationResponse = {
message: string;
description?: string;
};
export type PermissionResponse = unknown;
export type IdentityResponse = unknown;
export type CatalogActionCheckResponse = {
authenticated: boolean;
redirectTo?: string;
logout?: boolean;
error?: Error;
};
export type CatalogActionOnErrorResponse = {
redirectTo?: string;
logout?: boolean;
error?: Error;
};
export type CatalogActionResponse = {
success: boolean;
redirectTo?: string;
error?: Error;
[key: string]: unknown;
successNotification?: SuccessNotificationResponse;
};
export interface ICatalogActions {
listCatalog: (
dataSource: IDataSource,
pagination: {
pageIndex: number;
pageSize: number;
} = {
pageIndex: INITIAL_PAGE_INDEX,
pageSize: INITIAL_PAGE_SIZE,
},
quickSearchTerm: string = "",
): Promise<IListResponse_DTO<IListCustomerInvoices_Response_DTO>> => {
return dataProvider.getList({
resource: "invoices",
quickSearchTerm,
pagination,
});
};
}
}

View File

@ -0,0 +1,21 @@
import { usePagination } from "@/lib/hooks";
import { PropsWithChildren, createContext } from "react";
export interface ICatalogContextState {}
export const CatalogContext = createContext<ICatalogContextState | null>(null);
export const CatalogProvider = ({ children }: PropsWithChildren) => {
const [pagination, setPagination] = usePagination();
return (
<CatalogContext.Provider
value={{
pagination,
setPagination,
}}
>
{children}
</CatalogContext.Provider>
);
};

View File

@ -0,0 +1,36 @@
import { DataTable } from "@/components";
import { useDataTable } from "@/lib/hooks";
import { IListArticles_Response_DTO, IListResponse_DTO } from "@shared/contexts";
import { useMemo } from "react";
type CatalogTableViewProps = {
data: IListResponse_DTO<IListArticles_Response_DTO>;
};
export const CatalogDataTable = ({ data }: CatalogTableViewProps) => {
const columns = useMemo(
() => [
{
id: "description" as const,
accessorKey: "description",
size: 400,
enableHiding: false,
enableSorting: false,
enableResizing: false,
},
],
[]
);
const { table } = useDataTable({
data: data?.items ?? [],
columns: columns,
pageCount: data?.total_pages ?? -1,
});
return (
<>
<DataTable table={table} paginationOptions={{ visible: true }} />
</>
);
};

View File

@ -0,0 +1 @@
export * from "./CatalogDataTable";

View File

@ -0,0 +1,77 @@
import { IListArticles_Response_DTO } from "@shared/contexts";
import { ColumnDef } from "@tanstack/react-table";
import { DataTablaRowActionFunction, DataTableRowActions } from "@/components";
import { Badge } from "@/ui";
import { useMemo } from "react";
export const useCustomerInvoiceDataTableColumns = (
actions: DataTablaRowActionFunction<IListArticles_Response_DTO>
): ColumnDef<IListArticles_Response_DTO>[] => {
const customerColumns: ColumnDef<IListArticles_Response_DTO>[] = useMemo(
() => [
/*{
id: "complete_name",
header: "Nombre",
accessorFn: (row) => (
<div className="flex items-center justify-between space-x-4">
<div className="flex items-center space-x-4">
<Avatar>
<AvatarImage src={row.photo_url} />
<AvatarFallback>
{acronym(`${row.first_name} ${row.last_name}`)}
</AvatarFallback>
</Avatar>
<div>
<p className="text-base font-semibold">{`${row.first_name} ${row.last_name}`}</p>
<p className="mt-1">{row.job_title}</p>
</div>
</div>
</div>
),
enableSorting: true,
sortingFn: "alphanumeric",
enableHiding: false,
cell: ({ renderValue }) => (
<span className="w-full">
<>{renderValue()}</>
</span>
),
},*/
{
id: "company",
accessorKey: "company_name",
header: "Compañía",
enableSorting: true,
sortingFn: "alphanumeric",
},
{
id: "state",
accessorKey: "state",
header: "Estado",
cell: ({ renderValue }) => (
<Badge variant={"destructive"}>
<>{renderValue()}</>
</Badge>
),
},
{
id: "phone",
accessorKey: "phone",
header: "Phone",
enableSorting: true,
sortingFn: "alphanumeric",
},
{
id: "actions",
header: "Acciones",
cell: ({ row }) => {
return <DataTableRowActions row={row} actions={actions} />;
},
},
],
[actions]
);
return customerColumns;
};

View File

@ -0,0 +1 @@
export * from "./useCatalogList";

View File

@ -0,0 +1,40 @@
import { UseListQueryResult, useList } from "@/lib/hooks/useDataSource";
import { useDataSource } from "@/lib/hooks/useDataSource/useDataSource";
import { useQueryKey } from "@/lib/hooks/useQueryKey";
import { IListArticles_Response_DTO, IListResponse_DTO } from "@shared/contexts";
export type UseCatalogListParams = {
pagination: {
pageIndex: number;
pageSize: number;
};
searchTerm?: string;
enabled?: boolean;
queryOptions?: Record<string, unknown>;
};
export type UseCatalogListResponse = UseListQueryResult<
IListResponse_DTO<IListArticles_Response_DTO>,
unknown
>;
export const useCatalogList = (params: UseCatalogListParams): UseCatalogListResponse => {
const dataSource = useDataSource();
const keys = useQueryKey();
const { pagination, searchTerm = undefined, enabled = true, queryOptions } = params;
return useList({
queryKey: keys().data().resource("catalog").action("list").params(params).get(),
queryFn: () => {
console.log(pagination);
return dataSource.getList({
resource: "catalog",
quickSearchTerm: searchTerm,
pagination,
});
},
enabled,
queryOptions,
});
};

View File

@ -1 +1,2 @@
export * from "./layout";
export * from "./list";

View File

@ -0,0 +1,17 @@
import { Layout, LayoutContent, LayoutHeader } from "@/components";
import { DataTableProvider } from "@/lib/hooks";
import { PropsWithChildren } from "react";
import { CatalogProvider } from "./CatalogContext";
export const CatalogLayout = ({ children }: PropsWithChildren) => {
return (
<CatalogProvider>
<Layout>
<LayoutHeader />
<LayoutContent>
<DataTableProvider>{children}</DataTableProvider>
</LayoutContent>
</Layout>
</CatalogProvider>
);
};

View File

@ -28,289 +28,383 @@ import {
import { File, ListFilter, MoreHorizontal, PlusCircle } from "lucide-react";
import { Layout, LayoutContent, LayoutHeader } from "@/components";
import { DataTable, DataTableSkeleton, ErrorOverlay, SimpleEmptyState } from "@/components";
import { useDataTable, useDataTableContext } from "@/lib/hooks";
import { useMemo } from "react";
import { Trans } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { CatalogDataTable } from "./components";
import { useCatalogList } from "./hooks/useCatalogList";
export const CatalogList = () => {
return (
<Layout>
<LayoutHeader />
<LayoutContent>
<div className='flex items-center'>
<h1 className='text-lg font-semibold md:text-2xl'>Catalog</h1>
</div>
const navigate = useNavigate();
<Tabs defaultValue='all'>
<div className='flex items-center'>
<TabsList>
<TabsTrigger value='all'>All</TabsTrigger>
<TabsTrigger value='active'>Active</TabsTrigger>
<TabsTrigger value='draft'>Draft</TabsTrigger>
<TabsTrigger value='archived' className='hidden sm:flex'>
Archived
</TabsTrigger>
</TabsList>
<div className='flex items-center gap-2 ml-auto'>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='outline' size='sm' className='h-8 gap-1'>
<ListFilter className='h-3.5 w-3.5' />
<span className='sr-only sm:not-sr-only sm:whitespace-nowrap'>Filter</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Filter by</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem checked>Active</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem>Draft</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem>Archived</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
<Button size='sm' variant='outline' className='h-8 gap-1'>
<File className='h-3.5 w-3.5' />
<span className='sr-only sm:not-sr-only sm:whitespace-nowrap'>Export</span>
</Button>
<Button size='sm' className='h-8 gap-1'>
<PlusCircle className='h-3.5 w-3.5' />
<span className='sr-only sm:not-sr-only sm:whitespace-nowrap'>Add Product</span>
</Button>
</div>
const { pagination } = useDataTableContext();
console.log("pagination PADRE => ", pagination);
const { data, isPending, isError, error, refetch } = useCatalogList({
pagination: {
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize,
},
});
const columns = useMemo(
() => [
{
id: "description" as const,
accessorKey: "description",
size: 400,
enableHiding: false,
enableSorting: false,
enableResizing: false,
},
],
[]
);
const { table } = useDataTable({
data: data?.items ?? [],
columns: columns,
pageCount: data?.total_pages ?? -1,
});
return <DataTable table={table} paginationOptions={{ visible: true }} />;
if (isError || isPending) {
return <></>;
}
return (
<>
<Button
onClick={() => {
setPagination({
pageIndex: pagination.pageIndex + 1,
});
}}
>
+ Página
</Button>
{data.items.map((row) => (
<p>{row.description}</p>
))}
<CatalogDataTable data={data} />;
</>
);
if (isError) {
return <ErrorOverlay subtitle={(error as Error).message} />;
}
if (isPending) {
return (
<Card x-chunk='dashboard-06-chunk-0'>
<CardHeader>
<CardTitle>
<Trans i18nKey='catalog.title' />
</CardTitle>
<CardDescription>Manage your products and view their sales performance.</CardDescription>
</CardHeader>
<CardContent>
<DataTableSkeleton
columnCount={6}
searchableColumnCount={1}
filterableColumnCount={2}
//cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem"]}
shrinkZero
/>
</CardContent>
</Card>
);
}
if (data?.total_items === 0) {
return (
<SimpleEmptyState
subtitle='Empieza cargando los artículos del catálogo'
buttonText=''
onButtonClick={() => navigate("/catalog/add")}
/>
);
}
return (
<>
<Tabs defaultValue='all'>
<div className='flex items-center'>
<TabsList>
<TabsTrigger value='all'>All</TabsTrigger>
<TabsTrigger value='active'>Active</TabsTrigger>
<TabsTrigger value='draft'>Draft</TabsTrigger>
<TabsTrigger value='archived' className='hidden sm:flex'>
Archived
</TabsTrigger>
</TabsList>
<div className='flex items-center gap-2 ml-auto'>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='outline' size='sm' className='h-8 gap-1'>
<ListFilter className='h-3.5 w-3.5' />
<span className='sr-only sm:not-sr-only sm:whitespace-nowrap'>Filter</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Filter by</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem checked>Active</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem>Draft</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem>Archived</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
<Button size='sm' variant='outline' className='h-8 gap-1'>
<File className='h-3.5 w-3.5' />
<span className='sr-only sm:not-sr-only sm:whitespace-nowrap'>Export</span>
</Button>
<Button size='sm' className='h-8 gap-1'>
<PlusCircle className='h-3.5 w-3.5' />
<span className='sr-only sm:not-sr-only sm:whitespace-nowrap'>Add Product</span>
</Button>
</div>
<TabsContent value='all'>
<Card x-chunk='dashboard-06-chunk-0'>
<CardHeader>
<CardTitle>Products</CardTitle>
<CardDescription>
Manage your products and view their sales performance.
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead className='hidden w-[100px] sm:table-cell'>
<span className='sr-only'>Image</span>
</TableHead>
<TableHead>Name</TableHead>
<TableHead>Status</TableHead>
<TableHead className='hidden md:table-cell'>Price</TableHead>
<TableHead className='hidden md:table-cell'>Total Sales</TableHead>
<TableHead className='hidden md:table-cell'>Created at</TableHead>
<TableHead>
<span className='sr-only'>Actions</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className='hidden sm:table-cell'>
<img
alt='Product image'
className='object-cover rounded-md aspect-square'
height='64'
src='/placeholder.svg'
width='64'
/>
</TableCell>
<TableCell className='font-medium'>Laser Lemonade Machine</TableCell>
<TableCell>
<Badge variant='outline'>Draft</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>$499.99</TableCell>
<TableCell className='hidden md:table-cell'>25</TableCell>
<TableCell className='hidden md:table-cell'>2023-07-12 10:42 AM</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup='true' size='icon' variant='ghost'>
<MoreHorizontal className='w-4 h-4' />
<span className='sr-only'>Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='hidden sm:table-cell'>
<img
alt='Product image'
className='object-cover rounded-md aspect-square'
height='64'
src='/placeholder.svg'
width='64'
/>
</TableCell>
<TableCell className='font-medium'>Hypernova Headphones</TableCell>
<TableCell>
<Badge variant='outline'>Active</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>$129.99</TableCell>
<TableCell className='hidden md:table-cell'>100</TableCell>
<TableCell className='hidden md:table-cell'>2023-10-18 03:21 PM</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup='true' size='icon' variant='ghost'>
<MoreHorizontal className='w-4 h-4' />
<span className='sr-only'>Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='hidden sm:table-cell'>
<img
alt='Product image'
className='object-cover rounded-md aspect-square'
height='64'
src='/placeholder.svg'
width='64'
/>
</TableCell>
<TableCell className='font-medium'>AeroGlow Desk Lamp</TableCell>
<TableCell>
<Badge variant='outline'>Active</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>$39.99</TableCell>
<TableCell className='hidden md:table-cell'>50</TableCell>
<TableCell className='hidden md:table-cell'>2023-11-29 08:15 AM</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup='true' size='icon' variant='ghost'>
<MoreHorizontal className='w-4 h-4' />
<span className='sr-only'>Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='hidden sm:table-cell'>
<img
alt='Product image'
className='object-cover rounded-md aspect-square'
height='64'
src='/placeholder.svg'
width='64'
/>
</TableCell>
<TableCell className='font-medium'>TechTonic Energy Drink</TableCell>
<TableCell>
<Badge variant='secondary'>Draft</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>$2.99</TableCell>
<TableCell className='hidden md:table-cell'>0</TableCell>
<TableCell className='hidden md:table-cell'>2023-12-25 11:59 PM</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup='true' size='icon' variant='ghost'>
<MoreHorizontal className='w-4 h-4' />
<span className='sr-only'>Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='hidden sm:table-cell'>
<img
alt='Product image'
className='object-cover rounded-md aspect-square'
height='64'
src='/placeholder.svg'
width='64'
/>
</TableCell>
<TableCell className='font-medium'>Gamer Gear Pro Controller</TableCell>
<TableCell>
<Badge variant='outline'>Active</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>$59.99</TableCell>
<TableCell className='hidden md:table-cell'>75</TableCell>
<TableCell className='hidden md:table-cell'>2024-01-01 12:00 AM</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup='true' size='icon' variant='ghost'>
<MoreHorizontal className='w-4 h-4' />
<span className='sr-only'>Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='hidden sm:table-cell'>
<img
alt='Product image'
className='object-cover rounded-md aspect-square'
height='64'
src='/placeholder.svg'
width='64'
/>
</TableCell>
<TableCell className='font-medium'>Luminous VR Headset</TableCell>
<TableCell>
<Badge variant='outline'>Active</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>$199.99</TableCell>
<TableCell className='hidden md:table-cell'>30</TableCell>
<TableCell className='hidden md:table-cell'>2024-02-14 02:14 PM</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup='true' size='icon' variant='ghost'>
<MoreHorizontal className='w-4 h-4' />
<span className='sr-only'>Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
<CardFooter>
<div className='text-xs text-muted-foreground'>
Showing <strong>1-10</strong> of <strong>32</strong> products
</div>
</CardFooter>
</Card>
</TabsContent>
</Tabs>
</LayoutContent>
</Layout>
</div>
<TabsContent value='all'>
<Card x-chunk='dashboard-06-chunk-0'>
<CardHeader>
<CardTitle>
<Trans i18nKey='catalog.title' />
</CardTitle>
<CardDescription>
Manage your products and view their sales performance.
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead className='hidden w-[100px] sm:table-cell'>
<span className='sr-only'>Image</span>
</TableHead>
<TableHead>Name</TableHead>
<TableHead>Status</TableHead>
<TableHead className='hidden md:table-cell'>Price</TableHead>
<TableHead className='hidden md:table-cell'>Total Sales</TableHead>
<TableHead className='hidden md:table-cell'>Created at</TableHead>
<TableHead>
<span className='sr-only'>Actions</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className='hidden sm:table-cell'>
<img
alt='Product image'
className='object-cover rounded-md aspect-square'
height='64'
src='/placeholder.svg'
width='64'
/>
</TableCell>
<TableCell className='font-medium'>Laser Lemonade Machine</TableCell>
<TableCell>
<Badge variant='outline'>Draft</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>$499.99</TableCell>
<TableCell className='hidden md:table-cell'>25</TableCell>
<TableCell className='hidden md:table-cell'>2023-07-12 10:42 AM</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup='true' size='icon' variant='ghost'>
<MoreHorizontal className='w-4 h-4' />
<span className='sr-only'>Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='hidden sm:table-cell'>
<img
alt='Product image'
className='object-cover rounded-md aspect-square'
height='64'
src='/placeholder.svg'
width='64'
/>
</TableCell>
<TableCell className='font-medium'>Hypernova Headphones</TableCell>
<TableCell>
<Badge variant='outline'>Active</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>$129.99</TableCell>
<TableCell className='hidden md:table-cell'>100</TableCell>
<TableCell className='hidden md:table-cell'>2023-10-18 03:21 PM</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup='true' size='icon' variant='ghost'>
<MoreHorizontal className='w-4 h-4' />
<span className='sr-only'>Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='hidden sm:table-cell'>
<img
alt='Product image'
className='object-cover rounded-md aspect-square'
height='64'
src='/placeholder.svg'
width='64'
/>
</TableCell>
<TableCell className='font-medium'>AeroGlow Desk Lamp</TableCell>
<TableCell>
<Badge variant='outline'>Active</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>$39.99</TableCell>
<TableCell className='hidden md:table-cell'>50</TableCell>
<TableCell className='hidden md:table-cell'>2023-11-29 08:15 AM</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup='true' size='icon' variant='ghost'>
<MoreHorizontal className='w-4 h-4' />
<span className='sr-only'>Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='hidden sm:table-cell'>
<img
alt='Product image'
className='object-cover rounded-md aspect-square'
height='64'
src='/placeholder.svg'
width='64'
/>
</TableCell>
<TableCell className='font-medium'>TechTonic Energy Drink</TableCell>
<TableCell>
<Badge variant='secondary'>Draft</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>$2.99</TableCell>
<TableCell className='hidden md:table-cell'>0</TableCell>
<TableCell className='hidden md:table-cell'>2023-12-25 11:59 PM</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup='true' size='icon' variant='ghost'>
<MoreHorizontal className='w-4 h-4' />
<span className='sr-only'>Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='hidden sm:table-cell'>
<img
alt='Product image'
className='object-cover rounded-md aspect-square'
height='64'
src='/placeholder.svg'
width='64'
/>
</TableCell>
<TableCell className='font-medium'>Gamer Gear Pro Controller</TableCell>
<TableCell>
<Badge variant='outline'>Active</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>$59.99</TableCell>
<TableCell className='hidden md:table-cell'>75</TableCell>
<TableCell className='hidden md:table-cell'>2024-01-01 12:00 AM</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup='true' size='icon' variant='ghost'>
<MoreHorizontal className='w-4 h-4' />
<span className='sr-only'>Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='hidden sm:table-cell'>
<img
alt='Product image'
className='object-cover rounded-md aspect-square'
height='64'
src='/placeholder.svg'
width='64'
/>
</TableCell>
<TableCell className='font-medium'>Luminous VR Headset</TableCell>
<TableCell>
<Badge variant='outline'>Active</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>$199.99</TableCell>
<TableCell className='hidden md:table-cell'>30</TableCell>
<TableCell className='hidden md:table-cell'>2024-02-14 02:14 PM</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup='true' size='icon' variant='ghost'>
<MoreHorizontal className='w-4 h-4' />
<span className='sr-only'>Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
<CardFooter>
<div className='text-xs text-muted-foreground'>
Showing <strong>1-10</strong> of <strong>32</strong> products
</div>
</CardFooter>
</Card>
</TabsContent>
</Tabs>
</>
);
};

View File

@ -0,0 +1,8 @@
import { useContext } from "react";
import { CatalogContext } from "./CatalogContext";
export const useCatalogContext = () => {
const context = useContext(CatalogContext);
if (context === null) throw new Error("useCatalog must be used within a CatalogProvider");
return context;
};

View File

@ -10,6 +10,7 @@ import {
} from "lucide-react";
import { Layout, LayoutContent, LayoutHeader } from "@/components";
import { useGetIdentity } from "@/lib/hooks";
import {
Badge,
Button,
@ -42,383 +43,396 @@ import {
TabsList,
TabsTrigger,
} from "@/ui";
import { t } from "i18next";
export const DashboardPage = () => {
const { data, status } = useGetIdentity();
return (
<Layout>
<LayoutHeader />
<LayoutContent className='grid items-start flex-1 gap-4 p-4 sm:px-6 sm:py-0 md:gap-8 lg:grid-cols-3 xl:grid-cols-3'>
<div className='grid items-start gap-4 auto-rows-max md:gap-8 lg:col-span-2'>
<div className='grid gap-4 sm:grid-cols-2 md:grid-cols-4 lg:grid-cols-2 xl:grid-cols-4'>
<Card className='sm:col-span-2' x-chunk='dashboard-05-chunk-0'>
<CardHeader className='pb-3'>
<CardTitle>Tus Cotizaciones</CardTitle>
<CardDescription className='max-w-lg leading-relaxed text-balance'>
Introducing Our Dynamic Orders Dashboard for Seamless Management and Insightful
Analysis.
</CardDescription>
<LayoutContent>
{status === "success" && (
<div className='flex items-center'>
<h1 className='text-lg font-semibold md:text-2xl'>{`${t("dashboard.welcome")}, ${
data?.name
}`}</h1>
</div>
)}
<div className='grid items-start flex-1 gap-4 p-4 sm:px-6 sm:py-0 md:gap-8 lg:grid-cols-3 xl:grid-cols-3'>
<div className='grid items-start gap-4 auto-rows-max md:gap-8 lg:col-span-2'>
<div className='grid gap-4 sm:grid-cols-2 md:grid-cols-4 lg:grid-cols-2 xl:grid-cols-4'>
<Card className='sm:col-span-2' x-chunk='dashboard-05-chunk-0'>
<CardHeader className='pb-3'>
<CardTitle>Tus Cotizaciones</CardTitle>
<CardDescription className='max-w-lg leading-relaxed text-balance'>
Introducing Our Dynamic Orders Dashboard for Seamless Management and Insightful
Analysis.
</CardDescription>
</CardHeader>
<CardFooter>
<Button>Crear nueva cotización</Button>
</CardFooter>
</Card>
<Card x-chunk='dashboard-05-chunk-1'>
<CardHeader className='pb-2'>
<CardDescription>This Week</CardDescription>
<CardTitle className='text-4xl'>$1,329</CardTitle>
</CardHeader>
<CardContent>
<div className='text-xs text-muted-foreground'>+25% from last week</div>
</CardContent>
<CardFooter>
<Progress value={25} aria-label='25% increase' />
</CardFooter>
</Card>
<Card x-chunk='dashboard-05-chunk-2'>
<CardHeader className='pb-2'>
<CardDescription>This Month</CardDescription>
<CardTitle className='text-4xl'>$5,329</CardTitle>
</CardHeader>
<CardContent>
<div className='text-xs text-muted-foreground'>+10% from last month</div>
</CardContent>
<CardFooter>
<Progress value={12} aria-label='12% increase' />
</CardFooter>
</Card>
</div>
<Tabs defaultValue='week'>
<div className='flex items-center'>
<TabsList>
<TabsTrigger value='week'>Week</TabsTrigger>
<TabsTrigger value='month'>Month</TabsTrigger>
<TabsTrigger value='year'>Year</TabsTrigger>
</TabsList>
<div className='flex items-center gap-2 ml-auto'>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='outline' size='sm' className='gap-1 text-sm h-7'>
<ListFilter className='h-3.5 w-3.5' />
<span className='sr-only sm:not-sr-only'>Filter</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Filter by</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem checked>Fulfilled</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem>Declined</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem>Refunded</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
<Button size='sm' variant='outline' className='gap-1 text-sm h-7'>
<File className='h-3.5 w-3.5' />
<span className='sr-only sm:not-sr-only'>Export</span>
</Button>
</div>
</div>
<TabsContent value='week'>
<Card x-chunk='dashboard-05-chunk-3'>
<CardHeader className='px-7'>
<CardTitle>Orders</CardTitle>
<CardDescription>Recent orders from your store.</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Customer</TableHead>
<TableHead className='hidden sm:table-cell'>Type</TableHead>
<TableHead className='hidden sm:table-cell'>Status</TableHead>
<TableHead className='hidden md:table-cell'>Date</TableHead>
<TableHead className='text-right'>Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow className='bg-accent'>
<TableCell>
<div className='font-medium'>Liam Johnson</div>
<div className='hidden text-sm text-muted-foreground md:inline'>
liam@example.com
</div>
</TableCell>
<TableCell className='hidden sm:table-cell'>Sale</TableCell>
<TableCell className='hidden sm:table-cell'>
<Badge className='text-xs' variant='secondary'>
Fulfilled
</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>2023-06-23</TableCell>
<TableCell className='text-right'>$250.00</TableCell>
</TableRow>
<TableRow>
<TableCell>
<div className='font-medium'>Olivia Smith</div>
<div className='hidden text-sm text-muted-foreground md:inline'>
olivia@example.com
</div>
</TableCell>
<TableCell className='hidden sm:table-cell'>Refund</TableCell>
<TableCell className='hidden sm:table-cell'>
<Badge className='text-xs' variant='outline'>
Declined
</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>2023-06-24</TableCell>
<TableCell className='text-right'>$150.00</TableCell>
</TableRow>
<TableRow>
<TableCell>
<div className='font-medium'>Noah Williams</div>
<div className='hidden text-sm text-muted-foreground md:inline'>
noah@example.com
</div>
</TableCell>
<TableCell className='hidden sm:table-cell'>Subscription</TableCell>
<TableCell className='hidden sm:table-cell'>
<Badge className='text-xs' variant='secondary'>
Fulfilled
</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>2023-06-25</TableCell>
<TableCell className='text-right'>$350.00</TableCell>
</TableRow>
<TableRow>
<TableCell>
<div className='font-medium'>Emma Brown</div>
<div className='hidden text-sm text-muted-foreground md:inline'>
emma@example.com
</div>
</TableCell>
<TableCell className='hidden sm:table-cell'>Sale</TableCell>
<TableCell className='hidden sm:table-cell'>
<Badge className='text-xs' variant='secondary'>
Fulfilled
</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>2023-06-26</TableCell>
<TableCell className='text-right'>$450.00</TableCell>
</TableRow>
<TableRow>
<TableCell>
<div className='font-medium'>Liam Johnson</div>
<div className='hidden text-sm text-muted-foreground md:inline'>
liam@example.com
</div>
</TableCell>
<TableCell className='hidden sm:table-cell'>Sale</TableCell>
<TableCell className='hidden sm:table-cell'>
<Badge className='text-xs' variant='secondary'>
Fulfilled
</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>2023-06-23</TableCell>
<TableCell className='text-right'>$250.00</TableCell>
</TableRow>
<TableRow>
<TableCell>
<div className='font-medium'>Liam Johnson</div>
<div className='hidden text-sm text-muted-foreground md:inline'>
liam@example.com
</div>
</TableCell>
<TableCell className='hidden sm:table-cell'>Sale</TableCell>
<TableCell className='hidden sm:table-cell'>
<Badge className='text-xs' variant='secondary'>
Fulfilled
</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>2023-06-23</TableCell>
<TableCell className='text-right'>$250.00</TableCell>
</TableRow>
<TableRow>
<TableCell>
<div className='font-medium'>Olivia Smith</div>
<div className='hidden text-sm text-muted-foreground md:inline'>
olivia@example.com
</div>
</TableCell>
<TableCell className='hidden sm:table-cell'>Refund</TableCell>
<TableCell className='hidden sm:table-cell'>
<Badge className='text-xs' variant='outline'>
Declined
</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>2023-06-24</TableCell>
<TableCell className='text-right'>$150.00</TableCell>
</TableRow>
<TableRow>
<TableCell>
<div className='font-medium'>Emma Brown</div>
<div className='hidden text-sm text-muted-foreground md:inline'>
emma@example.com
</div>
</TableCell>
<TableCell className='hidden sm:table-cell'>Sale</TableCell>
<TableCell className='hidden sm:table-cell'>
<Badge className='text-xs' variant='secondary'>
Fulfilled
</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>2023-06-26</TableCell>
<TableCell className='text-right'>$450.00</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
<div>
<Card className='overflow-hidden' x-chunk='dashboard-05-chunk-4'>
<CardHeader className='flex flex-row items-start bg-muted/50'>
<div className='grid gap-0.5'>
<CardTitle className='flex items-center gap-2 text-lg group'>
Order Oe31b70H
<Button
size='icon'
variant='outline'
className='w-6 h-6 transition-opacity opacity-0 group-hover:opacity-100'
>
<Copy className='w-3 h-3' />
<span className='sr-only'>Copy Order ID</span>
</Button>
</CardTitle>
<CardDescription>Date: November 23, 2023</CardDescription>
</div>
<div className='flex items-center gap-1 ml-auto'>
<Button size='sm' variant='outline' className='h-8 gap-1'>
<Truck className='h-3.5 w-3.5' />
<span className='lg:sr-only xl:not-sr-only xl:whitespace-nowrap'>
Track Order
</span>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size='icon' variant='outline' className='w-8 h-8'>
<MoreVertical className='h-3.5 w-3.5' />
<span className='sr-only'>More</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Export</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Trash</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardHeader>
<CardFooter>
<Button>Crear nueva cotización</Button>
</CardFooter>
</Card>
<Card x-chunk='dashboard-05-chunk-1'>
<CardHeader className='pb-2'>
<CardDescription>This Week</CardDescription>
<CardTitle className='text-4xl'>$1,329</CardTitle>
</CardHeader>
<CardContent>
<div className='text-xs text-muted-foreground'>+25% from last week</div>
<CardContent className='p-6 text-sm'>
<div className='grid gap-3'>
<div className='font-semibold'>Order Details</div>
<ul className='grid gap-3'>
<li className='flex items-center justify-between'>
<span className='text-muted-foreground'>
Glimmer Lamps x <span>2</span>
</span>
<span>$250.00</span>
</li>
<li className='flex items-center justify-between'>
<span className='text-muted-foreground'>
Aqua Filters x <span>1</span>
</span>
<span>$49.00</span>
</li>
</ul>
<Separator className='my-2' />
<ul className='grid gap-3'>
<li className='flex items-center justify-between'>
<span className='text-muted-foreground'>Subtotal</span>
<span>$299.00</span>
</li>
<li className='flex items-center justify-between'>
<span className='text-muted-foreground'>Shipping</span>
<span>$5.00</span>
</li>
<li className='flex items-center justify-between'>
<span className='text-muted-foreground'>Tax</span>
<span>$25.00</span>
</li>
<li className='flex items-center justify-between font-semibold'>
<span className='text-muted-foreground'>Total</span>
<span>$329.00</span>
</li>
</ul>
</div>
<Separator className='my-4' />
<div className='grid grid-cols-2 gap-4'>
<div className='grid gap-3'>
<div className='font-semibold'>Shipping Information</div>
<address className='grid gap-0.5 not-italic text-muted-foreground'>
<span>Liam Johnson</span>
<span>1234 Main St.</span>
<span>Anytown, CA 12345</span>
</address>
</div>
<div className='grid gap-3 auto-rows-max'>
<div className='font-semibold'>Billing Information</div>
<div className='text-muted-foreground'>Same as shipping address</div>
</div>
</div>
<Separator className='my-4' />
<div className='grid gap-3'>
<div className='font-semibold'>Customer Information</div>
<dl className='grid gap-3'>
<div className='flex items-center justify-between'>
<dt className='text-muted-foreground'>Customer</dt>
<dd>Liam Johnson</dd>
</div>
<div className='flex items-center justify-between'>
<dt className='text-muted-foreground'>Email</dt>
<dd>
<a href='mailto:'>liam@acme.com</a>
</dd>
</div>
<div className='flex items-center justify-between'>
<dt className='text-muted-foreground'>Phone</dt>
<dd>
<a href='tel:'>+1 234 567 890</a>
</dd>
</div>
</dl>
</div>
<Separator className='my-4' />
<div className='grid gap-3'>
<div className='font-semibold'>Payment Information</div>
<dl className='grid gap-3'>
<div className='flex items-center justify-between'>
<dt className='flex items-center gap-1 text-muted-foreground'>
<CreditCard className='w-4 h-4' />
Visa
</dt>
<dd>**** **** **** 4532</dd>
</div>
</dl>
</div>
</CardContent>
<CardFooter>
<Progress value={25} aria-label='25% increase' />
</CardFooter>
</Card>
<Card x-chunk='dashboard-05-chunk-2'>
<CardHeader className='pb-2'>
<CardDescription>This Month</CardDescription>
<CardTitle className='text-4xl'>$5,329</CardTitle>
</CardHeader>
<CardContent>
<div className='text-xs text-muted-foreground'>+10% from last month</div>
</CardContent>
<CardFooter>
<Progress value={12} aria-label='12% increase' />
<CardFooter className='flex flex-row items-center px-6 py-3 border-t bg-muted/50'>
<div className='text-xs text-muted-foreground'>
Updated <time dateTime='2023-11-23'>November 23, 2023</time>
</div>
<Pagination className='w-auto ml-auto mr-0'>
<PaginationContent>
<PaginationItem>
<Button size='icon' variant='outline' className='w-6 h-6'>
<ChevronLeft className='h-3.5 w-3.5' />
<span className='sr-only'>Previous Order</span>
</Button>
</PaginationItem>
<PaginationItem>
<Button size='icon' variant='outline' className='w-6 h-6'>
<ChevronRight className='h-3.5 w-3.5' />
<span className='sr-only'>Next Order</span>
</Button>
</PaginationItem>
</PaginationContent>
</Pagination>
</CardFooter>
</Card>
</div>
<Tabs defaultValue='week'>
<div className='flex items-center'>
<TabsList>
<TabsTrigger value='week'>Week</TabsTrigger>
<TabsTrigger value='month'>Month</TabsTrigger>
<TabsTrigger value='year'>Year</TabsTrigger>
</TabsList>
<div className='flex items-center gap-2 ml-auto'>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='outline' size='sm' className='gap-1 text-sm h-7'>
<ListFilter className='h-3.5 w-3.5' />
<span className='sr-only sm:not-sr-only'>Filter</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Filter by</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem checked>Fulfilled</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem>Declined</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem>Refunded</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
<Button size='sm' variant='outline' className='gap-1 text-sm h-7'>
<File className='h-3.5 w-3.5' />
<span className='sr-only sm:not-sr-only'>Export</span>
</Button>
</div>
</div>
<TabsContent value='week'>
<Card x-chunk='dashboard-05-chunk-3'>
<CardHeader className='px-7'>
<CardTitle>Orders</CardTitle>
<CardDescription>Recent orders from your store.</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Customer</TableHead>
<TableHead className='hidden sm:table-cell'>Type</TableHead>
<TableHead className='hidden sm:table-cell'>Status</TableHead>
<TableHead className='hidden md:table-cell'>Date</TableHead>
<TableHead className='text-right'>Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow className='bg-accent'>
<TableCell>
<div className='font-medium'>Liam Johnson</div>
<div className='hidden text-sm text-muted-foreground md:inline'>
liam@example.com
</div>
</TableCell>
<TableCell className='hidden sm:table-cell'>Sale</TableCell>
<TableCell className='hidden sm:table-cell'>
<Badge className='text-xs' variant='secondary'>
Fulfilled
</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>2023-06-23</TableCell>
<TableCell className='text-right'>$250.00</TableCell>
</TableRow>
<TableRow>
<TableCell>
<div className='font-medium'>Olivia Smith</div>
<div className='hidden text-sm text-muted-foreground md:inline'>
olivia@example.com
</div>
</TableCell>
<TableCell className='hidden sm:table-cell'>Refund</TableCell>
<TableCell className='hidden sm:table-cell'>
<Badge className='text-xs' variant='outline'>
Declined
</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>2023-06-24</TableCell>
<TableCell className='text-right'>$150.00</TableCell>
</TableRow>
<TableRow>
<TableCell>
<div className='font-medium'>Noah Williams</div>
<div className='hidden text-sm text-muted-foreground md:inline'>
noah@example.com
</div>
</TableCell>
<TableCell className='hidden sm:table-cell'>Subscription</TableCell>
<TableCell className='hidden sm:table-cell'>
<Badge className='text-xs' variant='secondary'>
Fulfilled
</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>2023-06-25</TableCell>
<TableCell className='text-right'>$350.00</TableCell>
</TableRow>
<TableRow>
<TableCell>
<div className='font-medium'>Emma Brown</div>
<div className='hidden text-sm text-muted-foreground md:inline'>
emma@example.com
</div>
</TableCell>
<TableCell className='hidden sm:table-cell'>Sale</TableCell>
<TableCell className='hidden sm:table-cell'>
<Badge className='text-xs' variant='secondary'>
Fulfilled
</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>2023-06-26</TableCell>
<TableCell className='text-right'>$450.00</TableCell>
</TableRow>
<TableRow>
<TableCell>
<div className='font-medium'>Liam Johnson</div>
<div className='hidden text-sm text-muted-foreground md:inline'>
liam@example.com
</div>
</TableCell>
<TableCell className='hidden sm:table-cell'>Sale</TableCell>
<TableCell className='hidden sm:table-cell'>
<Badge className='text-xs' variant='secondary'>
Fulfilled
</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>2023-06-23</TableCell>
<TableCell className='text-right'>$250.00</TableCell>
</TableRow>
<TableRow>
<TableCell>
<div className='font-medium'>Liam Johnson</div>
<div className='hidden text-sm text-muted-foreground md:inline'>
liam@example.com
</div>
</TableCell>
<TableCell className='hidden sm:table-cell'>Sale</TableCell>
<TableCell className='hidden sm:table-cell'>
<Badge className='text-xs' variant='secondary'>
Fulfilled
</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>2023-06-23</TableCell>
<TableCell className='text-right'>$250.00</TableCell>
</TableRow>
<TableRow>
<TableCell>
<div className='font-medium'>Olivia Smith</div>
<div className='hidden text-sm text-muted-foreground md:inline'>
olivia@example.com
</div>
</TableCell>
<TableCell className='hidden sm:table-cell'>Refund</TableCell>
<TableCell className='hidden sm:table-cell'>
<Badge className='text-xs' variant='outline'>
Declined
</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>2023-06-24</TableCell>
<TableCell className='text-right'>$150.00</TableCell>
</TableRow>
<TableRow>
<TableCell>
<div className='font-medium'>Emma Brown</div>
<div className='hidden text-sm text-muted-foreground md:inline'>
emma@example.com
</div>
</TableCell>
<TableCell className='hidden sm:table-cell'>Sale</TableCell>
<TableCell className='hidden sm:table-cell'>
<Badge className='text-xs' variant='secondary'>
Fulfilled
</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>2023-06-26</TableCell>
<TableCell className='text-right'>$450.00</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
<div>
<Card className='overflow-hidden' x-chunk='dashboard-05-chunk-4'>
<CardHeader className='flex flex-row items-start bg-muted/50'>
<div className='grid gap-0.5'>
<CardTitle className='flex items-center gap-2 text-lg group'>
Order Oe31b70H
<Button
size='icon'
variant='outline'
className='w-6 h-6 transition-opacity opacity-0 group-hover:opacity-100'
>
<Copy className='w-3 h-3' />
<span className='sr-only'>Copy Order ID</span>
</Button>
</CardTitle>
<CardDescription>Date: November 23, 2023</CardDescription>
</div>
<div className='flex items-center gap-1 ml-auto'>
<Button size='sm' variant='outline' className='h-8 gap-1'>
<Truck className='h-3.5 w-3.5' />
<span className='lg:sr-only xl:not-sr-only xl:whitespace-nowrap'>
Track Order
</span>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size='icon' variant='outline' className='w-8 h-8'>
<MoreVertical className='h-3.5 w-3.5' />
<span className='sr-only'>More</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Export</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Trash</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardHeader>
<CardContent className='p-6 text-sm'>
<div className='grid gap-3'>
<div className='font-semibold'>Order Details</div>
<ul className='grid gap-3'>
<li className='flex items-center justify-between'>
<span className='text-muted-foreground'>
Glimmer Lamps x <span>2</span>
</span>
<span>$250.00</span>
</li>
<li className='flex items-center justify-between'>
<span className='text-muted-foreground'>
Aqua Filters x <span>1</span>
</span>
<span>$49.00</span>
</li>
</ul>
<Separator className='my-2' />
<ul className='grid gap-3'>
<li className='flex items-center justify-between'>
<span className='text-muted-foreground'>Subtotal</span>
<span>$299.00</span>
</li>
<li className='flex items-center justify-between'>
<span className='text-muted-foreground'>Shipping</span>
<span>$5.00</span>
</li>
<li className='flex items-center justify-between'>
<span className='text-muted-foreground'>Tax</span>
<span>$25.00</span>
</li>
<li className='flex items-center justify-between font-semibold'>
<span className='text-muted-foreground'>Total</span>
<span>$329.00</span>
</li>
</ul>
</div>
<Separator className='my-4' />
<div className='grid grid-cols-2 gap-4'>
<div className='grid gap-3'>
<div className='font-semibold'>Shipping Information</div>
<address className='grid gap-0.5 not-italic text-muted-foreground'>
<span>Liam Johnson</span>
<span>1234 Main St.</span>
<span>Anytown, CA 12345</span>
</address>
</div>
<div className='grid gap-3 auto-rows-max'>
<div className='font-semibold'>Billing Information</div>
<div className='text-muted-foreground'>Same as shipping address</div>
</div>
</div>
<Separator className='my-4' />
<div className='grid gap-3'>
<div className='font-semibold'>Customer Information</div>
<dl className='grid gap-3'>
<div className='flex items-center justify-between'>
<dt className='text-muted-foreground'>Customer</dt>
<dd>Liam Johnson</dd>
</div>
<div className='flex items-center justify-between'>
<dt className='text-muted-foreground'>Email</dt>
<dd>
<a href='mailto:'>liam@acme.com</a>
</dd>
</div>
<div className='flex items-center justify-between'>
<dt className='text-muted-foreground'>Phone</dt>
<dd>
<a href='tel:'>+1 234 567 890</a>
</dd>
</div>
</dl>
</div>
<Separator className='my-4' />
<div className='grid gap-3'>
<div className='font-semibold'>Payment Information</div>
<dl className='grid gap-3'>
<div className='flex items-center justify-between'>
<dt className='flex items-center gap-1 text-muted-foreground'>
<CreditCard className='w-4 h-4' />
Visa
</dt>
<dd>**** **** **** 4532</dd>
</div>
</dl>
</div>
</CardContent>
<CardFooter className='flex flex-row items-center px-6 py-3 border-t bg-muted/50'>
<div className='text-xs text-muted-foreground'>
Updated <time dateTime='2023-11-23'>November 23, 2023</time>
</div>
<Pagination className='w-auto ml-auto mr-0'>
<PaginationContent>
<PaginationItem>
<Button size='icon' variant='outline' className='w-6 h-6'>
<ChevronLeft className='h-3.5 w-3.5' />
<span className='sr-only'>Previous Order</span>
</Button>
</PaginationItem>
<PaginationItem>
<Button size='icon' variant='outline' className='w-6 h-6'>
<ChevronRight className='h-3.5 w-3.5' />
<span className='sr-only'>Next Order</span>
</Button>
</PaginationItem>
</PaginationContent>
</Pagination>
</CardFooter>
</Card>
</div>
</LayoutContent>
</Layout>

View File

@ -1 +1,2 @@
export * from "./layout";
export * from "./list";

View File

@ -0,0 +1,11 @@
import { Layout, LayoutContent, LayoutHeader } from "@/components";
import { PropsWithChildren } from "react";
export const DealerLayout = ({ children }: PropsWithChildren) => {
return (
<Layout>
<LayoutHeader />
<LayoutContent>{children}</LayoutContent>
</Layout>
);
};

View File

@ -1,14 +1,311 @@
import { Layout, LayoutContent, LayoutHeader } from "@/components";
import {
Badge,
Button,
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/ui";
import { File, ListFilter, MoreHorizontal, PlusCircle } from "lucide-react";
import { Trans } from "react-i18next";
export const DealersList = () => {
return (
<Layout>
<LayoutHeader />
<LayoutContent>
<>
<Tabs defaultValue='all'>
<div className='flex items-center'>
<h1 className='text-lg font-semibold md:text-2xl'>Dealers</h1>
<TabsList>
<TabsTrigger value='all'>All</TabsTrigger>
<TabsTrigger value='active'>Active</TabsTrigger>
<TabsTrigger value='draft'>Draft</TabsTrigger>
<TabsTrigger value='archived' className='hidden sm:flex'>
Archived
</TabsTrigger>
</TabsList>
<div className='flex items-center gap-2 ml-auto'>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='outline' size='sm' className='h-8 gap-1'>
<ListFilter className='h-3.5 w-3.5' />
<span className='sr-only sm:not-sr-only sm:whitespace-nowrap'>Filter</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Filter by</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem checked>Active</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem>Draft</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem>Archived</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
<Button size='sm' variant='outline' className='h-8 gap-1'>
<File className='h-3.5 w-3.5' />
<span className='sr-only sm:not-sr-only sm:whitespace-nowrap'>Export</span>
</Button>
<Button size='sm' className='h-8 gap-1'>
<PlusCircle className='h-3.5 w-3.5' />
<span className='sr-only sm:not-sr-only sm:whitespace-nowrap'>Add Product</span>
</Button>
</div>
</div>
</LayoutContent>
</Layout>
<TabsContent value='all'>
<Card x-chunk='dashboard-06-chunk-0'>
<CardHeader>
<CardTitle>
<Trans i18nKey='catalog.title' />
</CardTitle>
<CardDescription>
Manage your products and view their sales performance.
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead className='hidden w-[100px] sm:table-cell'>
<span className='sr-only'>Image</span>
</TableHead>
<TableHead>Name</TableHead>
<TableHead>Status</TableHead>
<TableHead className='hidden md:table-cell'>Price</TableHead>
<TableHead className='hidden md:table-cell'>Total Sales</TableHead>
<TableHead className='hidden md:table-cell'>Created at</TableHead>
<TableHead>
<span className='sr-only'>Actions</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className='hidden sm:table-cell'>
<img
alt='Product image'
className='object-cover rounded-md aspect-square'
height='64'
src='/placeholder.svg'
width='64'
/>
</TableCell>
<TableCell className='font-medium'>Laser Lemonade Machine</TableCell>
<TableCell>
<Badge variant='outline'>Draft</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>$499.99</TableCell>
<TableCell className='hidden md:table-cell'>25</TableCell>
<TableCell className='hidden md:table-cell'>2023-07-12 10:42 AM</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup='true' size='icon' variant='ghost'>
<MoreHorizontal className='w-4 h-4' />
<span className='sr-only'>Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='hidden sm:table-cell'>
<img
alt='Product image'
className='object-cover rounded-md aspect-square'
height='64'
src='/placeholder.svg'
width='64'
/>
</TableCell>
<TableCell className='font-medium'>Hypernova Headphones</TableCell>
<TableCell>
<Badge variant='outline'>Active</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>$129.99</TableCell>
<TableCell className='hidden md:table-cell'>100</TableCell>
<TableCell className='hidden md:table-cell'>2023-10-18 03:21 PM</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup='true' size='icon' variant='ghost'>
<MoreHorizontal className='w-4 h-4' />
<span className='sr-only'>Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='hidden sm:table-cell'>
<img
alt='Product image'
className='object-cover rounded-md aspect-square'
height='64'
src='/placeholder.svg'
width='64'
/>
</TableCell>
<TableCell className='font-medium'>AeroGlow Desk Lamp</TableCell>
<TableCell>
<Badge variant='outline'>Active</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>$39.99</TableCell>
<TableCell className='hidden md:table-cell'>50</TableCell>
<TableCell className='hidden md:table-cell'>2023-11-29 08:15 AM</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup='true' size='icon' variant='ghost'>
<MoreHorizontal className='w-4 h-4' />
<span className='sr-only'>Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='hidden sm:table-cell'>
<img
alt='Product image'
className='object-cover rounded-md aspect-square'
height='64'
src='/placeholder.svg'
width='64'
/>
</TableCell>
<TableCell className='font-medium'>TechTonic Energy Drink</TableCell>
<TableCell>
<Badge variant='secondary'>Draft</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>$2.99</TableCell>
<TableCell className='hidden md:table-cell'>0</TableCell>
<TableCell className='hidden md:table-cell'>2023-12-25 11:59 PM</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup='true' size='icon' variant='ghost'>
<MoreHorizontal className='w-4 h-4' />
<span className='sr-only'>Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='hidden sm:table-cell'>
<img
alt='Product image'
className='object-cover rounded-md aspect-square'
height='64'
src='/placeholder.svg'
width='64'
/>
</TableCell>
<TableCell className='font-medium'>Gamer Gear Pro Controller</TableCell>
<TableCell>
<Badge variant='outline'>Active</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>$59.99</TableCell>
<TableCell className='hidden md:table-cell'>75</TableCell>
<TableCell className='hidden md:table-cell'>2024-01-01 12:00 AM</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup='true' size='icon' variant='ghost'>
<MoreHorizontal className='w-4 h-4' />
<span className='sr-only'>Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='hidden sm:table-cell'>
<img
alt='Product image'
className='object-cover rounded-md aspect-square'
height='64'
src='/placeholder.svg'
width='64'
/>
</TableCell>
<TableCell className='font-medium'>Luminous VR Headset</TableCell>
<TableCell>
<Badge variant='outline'>Active</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>$199.99</TableCell>
<TableCell className='hidden md:table-cell'>30</TableCell>
<TableCell className='hidden md:table-cell'>2024-02-14 02:14 PM</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup='true' size='icon' variant='ghost'>
<MoreHorizontal className='w-4 h-4' />
<span className='sr-only'>Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
<CardFooter>
<div className='text-xs text-muted-foreground'>
Showing <strong>1-10</strong> of <strong>32</strong> products
</div>
</CardFooter>
</Card>
</TabsContent>
</Tabs>
</>
);
};

View File

@ -0,0 +1,14 @@
import { cn } from "@/lib/utils";
import React from "react";
export const ButtonGroup = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("ml-auto flex items-center gap-2", className)}
{...props}
/>
));
ButtonGroup.displayName = "ButtonGroup";

View File

@ -0,0 +1 @@
export * from "./ButtonGroup";

View File

@ -0,0 +1,31 @@
import { Button, ButtonProps } from "@/ui";
import { ChevronLeft } from "lucide-react";
import { To, useNavigate } from "react-router-dom";
export interface BackHistoryButtonProps extends ButtonProps {
label?: string;
url?: To;
}
export const BackHistoryButton = ({
label = "Volver atrás",
size,
url = undefined,
...props
}: BackHistoryButtonProps): JSX.Element => {
const navigate = useNavigate();
return (
<Button
variant="ghost"
onClick={() => {
url ? navigate(url) : navigate(-1);
}}
size={size}
{...props}
>
<ChevronLeft className="w-4 h-4" />
<span className={size === "icon" ? "sr-only" : "ml-2"}>{label}</span>
</Button>
);
};

View File

@ -0,0 +1 @@
export * from "./BackHistoryButton";

View File

@ -0,0 +1,97 @@
import { ColumnDef, Table as ReactTable, flexRender } from "@tanstack/react-table";
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/ui/table";
import { ReactNode } from "react";
import { Card, CardContent, CardFooter, CardHeader } from "@/ui";
import { DataTableColumnHeader } from "./DataTableColumnHeader";
import { DataTablePagination, DataTablePaginationProps } from "./DataTablePagination";
import { DataTableToolbar } from "./DataTableToolbar";
export type DataTableColumnProps<TData, TValue> = ColumnDef<TData, TValue>;
export type DataTablePaginationOptionsProps<TData> = Pick<
DataTablePaginationProps<TData>,
"visible"
>;
export type DataTableProps<TData> = {
table: ReactTable<TData>;
caption?: ReactNode;
className?: string;
paginationOptions?: DataTablePaginationOptionsProps<TData>;
};
export function DataTable<TData>({ table, caption, paginationOptions }: DataTableProps<TData>) {
return (
<>
<DataTableToolbar table={table} />
<Card>
<CardHeader>
<DataTablePagination
className='flex-1'
visible={paginationOptions?.visible}
table={table}
/>
</CardHeader>
<CardContent className='pt-6'>
<Table>
{typeof caption !== "undefined" && <TableCaption>{caption}</TableCaption>}
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead
key={header.id}
colSpan={header.colSpan}
style={{ width: header.getSize() }}
>
<DataTableColumnHeader table={table} header={header} />
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={table.getAllColumns.length} className='h-24 text-center'>
No hay datos para mostrar
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
<CardFooter>
<DataTablePagination
className='flex-1'
visible={paginationOptions?.visible}
table={table}
/>
</CardFooter>
</Card>
</>
);
}

View File

@ -0,0 +1,123 @@
import { Header, Table, flexRender } from "@tanstack/react-table";
import { cn } from "@/lib/utils";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
Separator,
} from "@/ui";
import {
ArrowDownIcon,
ArrowDownUpIcon,
ArrowUpIcon,
EyeOffIcon,
} from "lucide-react";
interface DataTableColumnHeaderProps<TData, TValue>
extends React.HTMLAttributes<HTMLDivElement> {
table: Table<TData>;
header: Header<TData, TValue>;
}
export function DataTableColumnHeader<TData, TValue>({
table,
header,
className,
}: DataTableColumnHeaderProps<TData, TValue>) {
if (!header.column.getCanSort()) {
return (
<>
<div
className={cn(
"data-[state=open]:bg-accent font-semiboldw text-muted-foreground uppercase",
className,
)}
>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</div>
{header.column.getCanResize() && (
<Separator
orientation="vertical"
className={cn(
"absolute top-0 h-full w-[5px] bg-black/10 cursor-col-resize",
table.options.columnResizeDirection,
header.column.getIsResizing() ? "bg-primary opacity-100" : "",
)}
{...{
onDoubleClick: () => header.column.resetSize(),
onMouseDown: header.getResizeHandler(),
onTouchStart: header.getResizeHandler(),
style: {
transform:
table.options.columnResizeMode === "onEnd" &&
header.column.getIsResizing()
? `translateX(${
(table.options.columnResizeDirection === "rtl"
? -1
: 1) *
(table.getState().columnSizingInfo.deltaOffset ?? 0)
}px)`
: "",
},
}}
/>
)}
</>
);
}
return (
<div className={cn("flex items-center space-x-2", className)}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
aria-label={
header.column.getIsSorted() === "desc"
? "En orden descendente. Click para ordenar ascendentemente."
: header.column.getIsSorted() === "asc"
? "En order ascendente. Click para ordenar descendentemente."
: "Sin orden. Click para ordenar ascendentemente."
}
size="sm"
variant="ghost"
className="-ml-3 h-8 data-[state=open]:bg-accent font-bold text-muted-foreground"
>
{flexRender(header.column.columnDef.header, header.getContext())}
{header.column.getIsSorted() === "desc" ? (
<ArrowDownIcon className="w-4 h-4 ml-2" />
) : header.column.getIsSorted() === "asc" ? (
<ArrowUpIcon className="w-4 h-4 ml-2" />
) : (
<ArrowDownUpIcon className="w-4 h-4 ml-2 text-muted-foreground/30" />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => header.column.toggleSorting(false)}>
<ArrowUpIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Ascendente
</DropdownMenuItem>
<DropdownMenuItem onClick={() => header.column.toggleSorting(true)}>
<ArrowDownIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Descendente
</DropdownMenuItem>
<DropdownMenuSeparator />
{header.column.getCanHide() && (
<DropdownMenuItem
onClick={() => header.column.toggleVisibility(false)}
>
<EyeOffIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Ocultar
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}

View File

@ -0,0 +1,149 @@
import { Column } from "@tanstack/react-table"
import * as React from "react"
import { cn } from "@/lib/utils"
import {
Badge,
Button,
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/ui"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/ui"
import { Separator } from "@/ui"
import { CheckIcon, PlusCircleIcon } from 'lucide-react'
interface DataTableFacetedFilterProps<TData, TValue> {
column?: Column<TData, TValue>
title?: string
options: {
label: string
value: string
icon?: React.ComponentType<{ className?: string }>
}[]
}
export function DataTableFacetedFilter<TData, TValue>({
column,
title,
options,
}: DataTableFacetedFilterProps<TData, TValue>) {
const facets = column?.getFacetedUniqueValues()
const selectedValues = new Set(column?.getFilterValue() as string[])
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm" className="h-8 border-dashed">
<PlusCircleIcon className="w-4 h-4 mr-2" />
{title}
{selectedValues?.size > 0 && (
<>
<Separator orientation="vertical" className="h-4 mx-2" />
<Badge
variant="secondary"
className="px-1 font-normal rounded-sm lg:hidden"
>
{selectedValues.size}
</Badge>
<div className="hidden space-x-1 lg:flex">
{selectedValues.size > 2 ? (
<Badge
variant="secondary"
className="px-1 font-normal rounded-sm"
>
{selectedValues.size} selected
</Badge>
) : (
options
.filter((option) => selectedValues.has(option.value))
.map((option) => (
<Badge
variant="secondary"
key={option.value}
className="px-1 font-normal rounded-sm"
>
{option.label}
</Badge>
))
)}
</div>
</>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<Command>
<CommandInput placeholder={title} />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
{options.map((option) => {
const isSelected = selectedValues.has(option.value)
return (
<CommandItem
key={option.value}
onSelect={() => {
if (isSelected) {
selectedValues.delete(option.value)
} else {
selectedValues.add(option.value)
}
const filterValues = Array.from(selectedValues)
column?.setFilterValue(
filterValues.length ? filterValues : undefined
)
}}
>
<div
className={cn(
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
isSelected
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible"
)}
>
<CheckIcon className={cn("h-4 w-4")} />
</div>
{option.icon && (
<option.icon className="w-4 h-4 mr-2 text-muted-foreground" />
)}
<span>{option.label}</span>
{facets?.get(option.value) && (
<span className="flex items-center justify-center w-4 h-4 ml-auto font-mono text-xs">
{facets.get(option.value)}
</span>
)}
</CommandItem>
)
})}
</CommandGroup>
{selectedValues.size > 0 && (
<>
<CommandSeparator />
<CommandGroup>
<CommandItem
onSelect={() => column?.setFilterValue(undefined)}
className="justify-center text-center"
>
Clear filters
</CommandItem>
</CommandGroup>
</>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}

View File

@ -0,0 +1,102 @@
import { DEFAULT_PAGE_SIZES, INITIAL_PAGE_INDEX } from "@/lib/hooks";
import { cn } from "@/lib/utils";
import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/ui";
import { Table } from "@tanstack/react-table";
import {
ChevronLeftIcon,
ChevronRightIcon,
ChevronsLeftIcon,
ChevronsRightIcon,
} from "lucide-react";
import { useMemo } from "react";
export type DataTablePaginationProps<TData> = {
table: Table<TData>;
className?: string;
visible?: boolean | "auto";
};
export function DataTablePagination<TData>({
table,
className,
visible = "auto",
}: DataTablePaginationProps<TData>) {
const isVisible = useMemo(() => visible === true, [visible]);
const isAuto = useMemo(() => visible === "auto", [visible]);
if (!isVisible || (isAuto && table.getPageCount() < 1)) {
return null;
}
return (
<div className={cn("flex items-center justify-between px-2", className)}>
<div className='flex-1 text-base text-muted-foreground'>
{table.getFilteredSelectedRowModel().rows.length} de{" "}
{table.getFilteredRowModel().rows.length} filas(s) seleccionadas.
</div>
<div className='flex items-center space-x-6 lg:space-x-8'>
<div className='flex items-center space-x-2'>
<p className='text-base font-medium'>Filas por página</p>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
>
<SelectTrigger className='h-8 w-[70px]'>
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side='top'>
{DEFAULT_PAGE_SIZES.map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className='flex w-[100px] items-center justify-center text-base font-medium'>
Página {table.getState().pagination.pageIndex + 1} de {table.getPageCount()}
</div>
<div className='flex items-center space-x-2'>
<Button
variant='outline'
className='hidden w-8 h-8 p-0 lg:flex'
onClick={() => table.setPageIndex(INITIAL_PAGE_INDEX)}
disabled={!table.getCanPreviousPage()}
>
<span className='sr-only'>Ir a la primera página</span>
<ChevronsLeftIcon className='w-4 h-4' />
</Button>
<Button
variant='outline'
className='w-8 h-8 p-0'
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className='sr-only'>Ir a la página anterior</span>
<ChevronLeftIcon className='w-4 h-4' />
</Button>
<Button
variant='outline'
className='w-8 h-8 p-0'
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className='sr-only'>Ir a la página siguiente</span>
<ChevronRightIcon className='w-4 h-4' />
</Button>
<Button
variant='outline'
className='hidden w-8 h-8 p-0 lg:flex'
onClick={() => table.setPageIndex(table.getPageCount() + 1)}
disabled={!table.getCanNextPage()}
>
<span className='sr-only'>Ir a la última página</span>
<ChevronsRightIcon className='w-4 h-4' />
</Button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,75 @@
"use client";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/ui";
import { CellContext } from "@tanstack/react-table";
import { MoreHorizontalIcon } from "lucide-react";
type DataTableRowActionContext<TData, TValue> = CellContext<TData, TValue>;
export type DataTablaRowActionFunction<TData, TValue> = (
props: DataTableRowActionContext<TData, TValue>,
) => DataTableRowActionDefinition<TData, TValue>[];
export type DataTableRowActionDefinition<TData, TValue> = {
label: string | "-";
shortcut?: string;
onClick?: (
props: DataTableRowActionContext<TData, TValue>,
e: React.BaseSyntheticEvent,
) => void;
};
export type DataTableRowActionsProps<TData, TValue> = {
props: DataTableRowActionContext<TData, TValue>;
actions?: DataTablaRowActionFunction<TData, TValue>;
};
export function DataTableRowActions<TData, TValue>({
actions,
...props
}: DataTableRowActionsProps<TData, TValue>) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
aria-haspopup="true"
size="icon"
variant="link"
className="w-4 h-4 translate-y-[2px]"
>
<MoreHorizontalIcon className="w-4 h-4" />
<span className="sr-only">Abrir menú</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Acciones</DropdownMenuLabel>
{actions &&
actions(props).map((action, index) =>
action.label === "-" ? (
<DropdownMenuSeparator key={index} />
) : (
<DropdownMenuItem
key={index}
onClick={(event) =>
action.onClick ? action.onClick(props, event) : null
}
>
{action.label}
<DropdownMenuShortcut>{action.shortcut}</DropdownMenuShortcut>
</DropdownMenuItem>
),
)}
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -0,0 +1,30 @@
import { cn } from "@/lib/utils";
import { Button } from "@/ui";
import { useSortable } from "@dnd-kit/sortable";
import { GripVerticalIcon } from "lucide-react";
export const DataTableRowDragHandleCell = ({ rowId }: { rowId: string }) => {
const { attributes, listeners, isDragging } = useSortable({
id: rowId,
});
return (
<Button
onClick={(event) => {
event.preventDefault();
return;
}}
size="icon"
variant="link"
className={cn(
isDragging ? "cursor-grabbing" : "cursor-grab",
"w-4 h-4 translate-y-[2px]"
)}
{...attributes}
{...listeners}
>
<GripVerticalIcon className="w-4 h-4" />
<span className="sr-only">Mover fila</span>
</Button>
);
};

View File

@ -0,0 +1,36 @@
import { Checkbox } from "@/ui";
import { DataTableColumnProps } from "./DataTable";
export function getDataTableSelectionColumn<
TData,
TError,
>(): DataTableColumnProps<TData, TError> {
return {
id: "select",
header: ({ table }) => (
<Checkbox
id="select-all"
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Seleccionar todo"
className="translate-y-[2px]"
/>
),
cell: ({ row, table }) => (
<Checkbox
id={`select-row-${row.id}`}
checked={row.getIsSelected()}
onCheckedChange={(value) => {
row.toggleSelected(!!value);
}}
aria-label="Seleccionar file"
className="translate-y-[2px]"
/>
),
enableSorting: false,
enableHiding: false,
};
}

View File

@ -0,0 +1,149 @@
import {
Skeleton,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/ui";
interface DataTableSkeletonProps {
/**
* The number of columns in the table.
* @type number
*/
columnCount: number;
/**
* The number of rows in the table.
* @default 10
* @type number | undefined
*/
rowCount?: number;
/**
* The number of searchable columns in the table.
* @default 0
* @type number | undefined
*/
searchableColumnCount?: number;
/**
* The number of filterable columns in the table.
* @default 0
* @type number | undefined
*/
filterableColumnCount?: number;
/**
* Flag to show the table view options.
* @default undefined
* @type boolean | undefined
*/
showViewOptions?: boolean;
/**
* The width of each cell in the table.
* The length of the array should be equal to the columnCount.
* Any valid CSS width value is accepted.
* @default ["auto"]
* @type string[] | undefined
*/
cellWidths?: string[];
/**
* Flag to prevent the table from shrinking to fit the content.
* @default false
* @type boolean | undefined
*/
shrinkZero?: boolean;
}
export function DataTableSkeleton({
columnCount,
rowCount = 10,
searchableColumnCount = 0,
filterableColumnCount = 0,
showViewOptions = true,
cellWidths = ["auto"],
shrinkZero = false,
}: DataTableSkeletonProps) {
return (
<div className="w-full space-y-3 overflow-auto">
<div className="flex items-center justify-between w-full p-1 space-x-2 overflow-auto">
<div className="flex items-center flex-1 space-x-2">
{searchableColumnCount > 0
? Array.from({ length: searchableColumnCount }).map((_, i) => (
<Skeleton key={i} className="w-40 h-7 lg:w-60" />
))
: null}
{filterableColumnCount > 0
? Array.from({ length: filterableColumnCount }).map((_, i) => (
<Skeleton key={i} className="h-7 w-[4.5rem] border-dashed" />
))
: null}
</div>
{showViewOptions ? (
<Skeleton className="ml-auto hidden h-7 w-[4.5rem] lg:flex" />
) : null}
</div>
<div className="border rounded-md">
<Table>
<TableHeader>
{Array.from({ length: 1 }).map((_, i) => (
<TableRow key={i} className="hover:bg-transparent">
{Array.from({ length: columnCount }).map((_, j) => (
<TableHead
key={j}
style={{
width: cellWidths[j],
minWidth: shrinkZero ? cellWidths[j] : "auto",
}}
>
<Skeleton className="w-full h-6" />
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{Array.from({ length: rowCount }).map((_, i) => (
<TableRow key={i} className="hover:bg-transparent">
{Array.from({ length: columnCount }).map((_, j) => (
<TableCell
key={j}
style={{
width: cellWidths[j],
minWidth: shrinkZero ? cellWidths[j] : "auto",
}}
>
<Skeleton className="w-full h-6" />
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
<div className="flex flex-col-reverse items-center justify-between w-full gap-4 px-2 py-1 overflow-auto sm:flex-row sm:gap-8">
<Skeleton className="w-40 h-8" />
<div className="flex flex-col-reverse items-center gap-4 sm:flex-row sm:gap-6 lg:gap-8">
<div className="flex items-center space-x-2">
<Skeleton className="w-24 h-8" />
<Skeleton className="h-8 w-[4.5rem]" />
</div>
<div className="flex items-center justify-center text-sm font-medium">
<Skeleton className="w-20 h-8" />
</div>
<div className="flex items-center space-x-2">
<Skeleton className="hidden size-8 lg:block" />
<Skeleton className="size-8" />
<Skeleton className="size-8" />
<Skeleton className="hidden size-8 lg:block" />
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,57 @@
import { Table } from "@tanstack/react-table";
import {
Button,
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/ui";
import { Settings2Icon } from "lucide-react";
interface DataTableColumnOptionsProps<TData> {
table: Table<TData>;
}
export function DataTableColumnOptions<TData>({
table,
}: DataTableColumnOptionsProps<TData>) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className="hidden h-8 ml-auto lg:flex"
>
<Settings2Icon className="w-4 h-4 mr-2" />
Columnas
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[150px]">
<DropdownMenuLabel>Columnas</DropdownMenuLabel>
<DropdownMenuSeparator />
{table
.getAllColumns()
.filter(
(column) =>
typeof column.accessorFn !== "undefined" && column.getCanHide()
)
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
<>{column.columnDef.header}</>
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -0,0 +1,138 @@
import { Table } from "@tanstack/react-table";
//import { priorities, statuses } from './Data'
import { DataTableFilterField } from "@/lib/types";
import { cn } from "@/lib/utils";
import { Button, Input } from "@/ui";
import { CrossIcon } from "lucide-react";
import { useMemo } from "react";
import { DataTableFacetedFilter } from "../DataTableFacetedFilter";
import { DataTableColumnOptions } from "./DataTableColumnOptions";
interface DataTableToolbarProps<TData>
extends React.HTMLAttributes<HTMLDivElement> {
table: Table<TData>;
filterFields?: DataTableFilterField<TData>[];
}
export function DataTableToolbar<TData>({
table,
filterFields = [],
children,
className,
...props
}: DataTableToolbarProps<TData>) {
const isFiltered = table.getState().columnFilters.length > 0;
// Memoize computation of searchableColumns and filterableColumns
const { searchableColumns, filterableColumns } = useMemo(() => {
return {
searchableColumns: filterFields.filter((field) => !field.options),
filterableColumns: filterFields.filter((field) => field.options),
};
}, [filterFields]);
return (
<div
className={cn(
"flex w-full items-center justify-between space-x-2 overflow-auto p-1",
className,
)}
{...props}
>
<div className="flex items-center flex-1 space-x-2">
{searchableColumns.length > 0 &&
searchableColumns.map(
(column) =>
table.getColumn(column.value ? String(column.value) : "") && (
<Input
key={String(column.value)}
placeholder={column.placeholder}
value={
(table
.getColumn(String(column.value))
?.getFilterValue() as string) ?? ""
}
onChange={(event) =>
table
.getColumn(String(column.value))
?.setFilterValue(event.target.value)
}
className="w-40 h-8 lg:w-64"
/>
),
)}
{filterableColumns.length > 0 &&
filterableColumns.map(
(column) =>
table.getColumn(column.value ? String(column.value) : "") && (
<DataTableFacetedFilter
key={String(column.value)}
column={table.getColumn(
column.value ? String(column.value) : "",
)}
title={column.label}
options={column.options ?? []}
/>
),
)}
{isFiltered && (
<Button
variant="ghost"
onClick={() => table.resetColumnFilters()}
className="h-8 px-2 lg:px-3"
>
Reset filters
<CrossIcon className="w-4 h-4 ml-2" />
</Button>
)}
</div>
<div className="flex items-center gap-2">
{children}
{table.options.enableHiding && <DataTableColumnOptions table={table} />}
</div>
</div>
);
/*
return (
<div className="flex items-center justify-between">
<div className="flex items-center flex-1 space-x-2">
<Input
placeholder="Filter tasks..."
value={(table.getColumn("customer")?.getFilterValue() as string) ?? ""}
onChange={(event) =>
table.getColumn("customer")?.setFilterValue(event.target.value)
}
className="h-8 w-[150px] lg:w-[250px]"
/>
{table.getColumn("status") && (
<DataTableFacetedFilter
column={table.getColumn("status")}
title="Status"
options={statuses}
/>
)}
{table.getColumn("priority") && (
<DataTableFacetedFilter
column={table.getColumn("priority")}
title="Priority"
options={priorities}
/>
)}
{isFiltered && (
<Button
variant="ghost"
onClick={() => table.resetColumnFilters()}
className="h-8 px-2 lg:px-3"
>
Reset
<CrossIcon className="w-4 h-4 ml-2" />
</Button>
)}
</div>
<DataTableViewOptions table={table} />
</div>
)
*/
}

View File

@ -0,0 +1 @@
export * from "./DataTableToolbar";

View File

@ -0,0 +1,6 @@
export * from "./DataTable";
export * from "./DataTableColumnHeader";
export * from "./DataTableRowActions";
export * from "./DataTableRowDragHandleCell";
export * from "./DataTableSelectionColumn";
export * from "./DataTableSkeleton";

View File

@ -0,0 +1,42 @@
import { Button } from "@/ui";
import { PlusIcon } from "lucide-react";
export const SimpleEmptyState = ({
title = "Esto está muy vacío",
subtitle = "Empieza dando de alta un item",
buttonText = "Nuevo item",
onButtonClick = () => {},
actions = undefined,
}) => {
return (
<div className="text-center">
<svg
className="w-12 h-12 mx-auto text-slate-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
vectorEffect="non-scaling-stroke"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"
/>
</svg>
<h3 className="mt-2 text-sm font-semibold text-slate-900">{title}</h3>
<p className="mt-1 text-sm text-slate-500">{subtitle}</p>
<div className="items-center mt-6">
{actions && <>{actions}</>}
{!actions && (
<Button className="my-4" onClick={onButtonClick}>
<PlusIcon />
{buttonText}
</Button>
)}
</div>
</div>
);
};

View File

@ -0,0 +1 @@
export * from "./SimpleEmptyState";

View File

@ -0,0 +1,20 @@
@tailwind components;
@layer components {
.ErrorOverlay {
@apply fixed top-0 bottom-0 left-0 right-0 z-50 w-full h-screen overflow-hidden;
@apply flex justify-center bg-slate-700 opacity-75;
}
.ErrorOverlay__container {
@apply flex flex-col items-center justify-center max-w-xs;
}
.ErrorOverlay__title {
@apply mt-6 text-xl font-semibold text-center text-white;
}
.ErrorOverlay__subtitle {
@apply text-center text-white;
}
}

View File

@ -0,0 +1,53 @@
import { Button } from "@/ui";
import { HomeIcon } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { BackHistoryButton } from "../CustomButtons";
interface ErrorOverlayProps {
title?: string;
subtitle?: string;
description?: string;
errorMessage?: string;
//errorStatusCode?: number | string | undefined;
}
/*const _DrawByStatusCode = {
0: UndrawAutumn,
404: UndrawNoData, //UndrawEmpty,
};*/
export const ErrorOverlay = ({
title = "Se ha producido un error",
subtitle = undefined,
description = undefined,
errorMessage = undefined,
}: //errorStatusCode = undefined,
ErrorOverlayProps): JSX.Element => {
const navigate = useNavigate();
/*const ErrorIllustration = () =>
String(errorStatusCode).length
? _DrawByStatusCode[String(errorStatusCode)]
: _DrawByStatusCode['0'];*/
return (
<div className='grid h-screen place-items-center '>
<div className='text-center'>
<h2 className='mt-2 text-xl font-semibold text-center text-slate-900'>{title}</h2>
<p className='mt-1 font-medium text-slate-500'>
{subtitle || errorMessage}
<br />
{description}
</p>
<div className='mt-6 space-x-8 md:ml-4'>
<BackHistoryButton />
<Button onClick={() => navigate("/")}>
<HomeIcon className='w-4 h-4 mr-2' /> Ir al inicio
</Button>
</div>
</div>
</div>
);
};
ErrorOverlay.displayName = "ErrorOverlay";

View File

@ -0,0 +1 @@
export * from './ErrorOverlay';

View File

@ -0,0 +1,30 @@
import { cn } from "@/lib/utils";
import { Button, ButtonProps } from "@/ui";
import { PlusCircleIcon } from "lucide-react";
export interface AddNewRowButtonProps extends ButtonProps {
label?: string;
className?: string;
}
export const AddNewRowButton = ({
label = "Añade nueva fila",
className,
...props
}: AddNewRowButtonProps): JSX.Element => (
<Button
type="button"
variant="outline"
size="icon"
className={cn(
"w-full gap-1 border-dashed text-muted-foreground border-muted-foreground/50",
className
)}
{...props}
>
<PlusCircleIcon className={label ? "w-4 h-4 mr-2" : "w-4 h-4"} />
{label && <>{label}</>}
</Button>
);
AddNewRowButton.displayName = "AddNewRowButton";

View File

@ -0,0 +1,463 @@
import { DataTableColumnHeader } from "@/components";
import { Badge } from "@/ui";
import {
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "@/ui/table";
import {
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
DropAnimation,
KeyboardSensor,
MeasuringStrategy,
MouseSensor,
PointerSensor,
TouchSensor,
UniqueIdentifier,
closestCenter,
defaultDropAnimation,
useSensor,
useSensors,
} from "@dnd-kit/core";
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import {
ColumnDef,
Row,
RowData,
RowSelectionState,
VisibilityState,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import { useCallback, useMemo, useState } from "react";
import { createPortal } from "react-dom";
import { FieldValues, UseFieldArrayReturn } from "react-hook-form";
import { AddNewRowButton } from "./AddNewRowButton";
import { SortableDataTableToolbar } from "./SortableDataTableToolbar";
import { SortableTableRow } from "./SortableTableRow";
declare module "@tanstack/react-table" {
interface TableMeta<TData extends RowData> {
insertItem: (rowIndex: number, data: TData) => void;
appendItem: (data: TData) => void;
duplicateItems: (rowIndex?: number) => void;
deleteItems: (rowIndex?: number | number[]) => void;
updateItem: (rowIndex: number, rowData: TData, fieldName: string, value: unknown) => void;
}
}
export interface SortableProps {
id: UniqueIdentifier;
}
export type SortableDataTableProps = {
columns: ColumnDef<unknown, unknown>[];
data: Record<"id", string>[];
actions: Omit<UseFieldArrayReturn<FieldValues, "items">, "fields">;
};
const measuringConfig = {
droppable: {
strategy: MeasuringStrategy.Always,
},
};
const dropAnimationConfig: DropAnimation = {
keyframes({ transform }) {
return [
{ opacity: 1, transform: CSS.Transform.toString(transform.initial) },
{
opacity: 0,
transform: CSS.Transform.toString({
...transform.final,
x: transform.final.x + 5,
y: transform.final.y + 5,
}),
},
];
},
easing: "ease-out",
sideEffects({ active }) {
active.node.animate([{ opacity: 0 }, { opacity: 1 }], {
duration: defaultDropAnimation.duration,
easing: defaultDropAnimation.easing,
});
},
};
/*const defaultColumn: Partial<ColumnDef<unknown>> = {
cell: ({ table, row: { index, original }, column, getValue }) => {
const initialValue = getValue();
// We need to keep and update the state of the cell normally
// eslint-disable-next-line react-hooks/rules-of-hooks
const [value, setValue] = useState(initialValue);
// If the initialValue is changed external, sync it up with our state
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return (
<input
value={value as string}
onChange={(e) => setValue(e.target.value)}
onBlur={() => {
console.log(column.id, value);
table.options.meta?.updateItem(index, original, column.id, value);
}}
/>
);
},
};*/
export function SortableDataTable({ columns, data, actions }: SortableDataTableProps) {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [activeId, setActiveId] = useState<UniqueIdentifier | null>();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const sorteableRowIds = useMemo(() => data.map((item) => item.id), [data]);
const table = useReactTable({
data,
columns,
enableColumnResizing: false,
columnResizeMode: "onChange",
//defaultColumn,
state: {
rowSelection,
columnVisibility,
},
enableRowSelection: true,
enableMultiRowSelection: true,
enableSorting: false,
enableHiding: true,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
getRowId: (originalRow: unknown) => originalRow?.id,
debugHeaders: true,
debugColumns: true,
meta: {
insertItem: (rowIndex: number, data: object = {}) => {
actions.insert(rowIndex, data, { shouldFocus: true });
},
appendItem: (data: object = {}) => {
actions.append(data, { shouldFocus: true });
},
duplicateItems: (rowIndex?: number) => {
if (rowIndex != undefined) {
const originalData = table.getRowModel().rows[rowIndex].original;
actions.insert(rowIndex + 1, originalData, { shouldFocus: true });
} else if (table.getSelectedRowModel().rows.length) {
const lastIndex =
table.getSelectedRowModel().rows[table.getSelectedRowModel().rows.length - 1].index;
const data = table
.getSelectedRowModel()
.rows.map((row) => ({ ...row.original, id: undefined }));
if (table.getRowModel().rows.length < lastIndex + 1) {
actions.append(data);
} else {
actions.insert(lastIndex + 1, data, { shouldFocus: true });
}
table.resetRowSelection();
}
},
deleteItems: (rowIndex?: number | number[]) => {
if (rowIndex != undefined) {
actions.remove(rowIndex);
} else if (table.getSelectedRowModel().rows.length > 0) {
let start = table.getSelectedRowModel().rows.length - 1;
for (; start >= 0; start--) {
const oldIndex = sorteableRowIds.indexOf(
String(table.getSelectedRowModel().rows[start].id)
);
actions.remove(oldIndex);
sorteableRowIds.splice(oldIndex, 1);
}
/*table.getSelectedRowModel().rows.forEach((row) => {
});*/
table.resetRowSelection();
} else {
actions.remove();
}
},
updateItem: (rowIndex: number, rowData: unknown, fieldName: string, value: unknown) => {
// Skip page index reset until after next rerender
// skipAutoResetPageIndex();
console.log({
rowIndex,
rowData,
fieldName,
value,
});
actions.update(rowIndex, { ...rowData, [`${fieldName}`]: value });
},
},
});
const sensors = useSensors(
useSensor(MouseSensor, {}),
useSensor(TouchSensor, {}),
useSensor(KeyboardSensor, {}),
useSensor(PointerSensor, {})
);
function handleDragEnd(event: DragEndEvent) {
let activeId = event.active.id;
let overId = event.over?.id;
if (overId !== undefined && activeId !== overId) {
let newIndex = sorteableRowIds.indexOf(String(overId));
if (table.getSelectedRowModel().rows.length > 1) {
table.getSelectedRowModel().rows.forEach((row, index) => {
const oldIndex = sorteableRowIds.indexOf(String(row.id));
if (index > 0) {
activeId = row.id;
newIndex = sorteableRowIds.indexOf(String(overId));
if (newIndex < oldIndex) {
newIndex = newIndex + 1;
}
}
actions.move(oldIndex, newIndex);
sorteableRowIds.splice(newIndex, 0, sorteableRowIds.splice(oldIndex, 1)[0]);
overId = row.id;
});
} else {
const oldIndex = sorteableRowIds.indexOf(String(activeId));
actions.move(oldIndex, newIndex);
}
}
setActiveId(null);
}
function handleDragStart({ active }: DragStartEvent) {
if (!table.getSelectedRowModel().rowsById[active.id]) {
table.resetRowSelection();
}
setActiveId(active.id);
}
function handleDragCancel() {
setActiveId(null);
}
const hadleNewItem = useCallback(() => {
actions.append([
{
description: "a",
},
{
description: "b",
},
{
description: "c",
},
{
description: "d",
},
]);
}, [actions]);
function filterItems(items: string[] | Row<unknown>[]) {
if (!activeId) {
return items;
}
return items.filter((idOrRow) => {
const id = typeof idOrRow === "string" ? idOrRow : idOrRow.id;
return id === activeId || !table.getSelectedRowModel().rowsById[id];
});
}
return (
<DndContext
measuring={measuringConfig}
sensors={sensors}
onDragEnd={handleDragEnd}
onDragStart={handleDragStart}
onDragCancel={handleDragCancel}
collisionDetection={closestCenter}
>
<SortableDataTableToolbar table={table} />
<Table className='table-auto'>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className='hover:bg-transparent'>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} className='px-1'>
{header.isPlaceholder ? null : (
<DataTableColumnHeader table={table} header={header} />
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
<SortableContext
items={filterItems(sorteableRowIds)}
strategy={verticalListSortingStrategy}
>
{filterItems(table.getRowModel().rows).map((row) => (
<SortableTableRow key={row.id} id={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell
className='p-1 align-top'
key={cell.id}
style={{ width: cell.column.getSize() }}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</SortableTableRow>
))}
</SortableContext>
</TableBody>
<TableFooter className='bg-default'>
<TableRow className='hover:bg-default'>
<TableCell colSpan={6} className='py-6'>
<AddNewRowButton onClick={hadleNewItem} />
</TableCell>
</TableRow>
</TableFooter>
</Table>
{createPortal(
<DragOverlay dropAnimation={dropAnimationConfig} className={"z-40 opacity-100"}>
{activeId && (
<div className='relative flex flex-wrap'>
{table.getSelectedRowModel().rows.length ? (
<Badge
variant='destructive'
className='absolute z-50 flex items-center justify-center w-2 h-2 p-3 rounded-full top left -left-2 -top-2'
>
{table.getSelectedRowModel().rows.length}
</Badge>
) : null}
<div className='absolute z-40 bg-white border rounded shadow opacity-100 top left hover:bg-white border-muted-foreground/50'>
<Table>
<TableBody>
{table.getRowModel().rows.map(
(row) =>
row.id === activeId && (
<TableRow key={row.id} id={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell
className='p-1 align-top'
key={cell.id}
style={{ width: cell.column.getSize() }}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
)
)}
</TableBody>
</Table>
</div>
{table.getSelectedRowModel().rows.length > 1 && (
<div className='absolute z-30 transform -translate-x-1 translate-y-1 bg-white border rounded shadow opacity-100 hover:bg-white border-muted-foreground/50 top left rotate-1'>
<Table>
<TableBody>
{table.getRowModel().rows.map(
(row) =>
row.id === activeId && (
<TableRow key={row.id} id={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell
className='p-1 align-top'
key={cell.id}
style={{ width: cell.column.getSize() }}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
)
)}
</TableBody>
</Table>
</div>
)}
{table.getSelectedRowModel().rows.length > 2 && (
<div className='absolute z-20 transform translate-x-1 -translate-y-1 bg-white border rounded shadow opacity-100 hover:bg-white border-muted-foreground/50 top left -rotate-1'>
<Table>
<TableBody>
{table.getRowModel().rows.map(
(row) =>
row.id === activeId && (
<TableRow key={row.id} id={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell
className='p-1 align-top'
key={cell.id}
style={{ width: cell.column.getSize() }}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
)
)}
</TableBody>
</Table>
</div>
)}
{table.getSelectedRowModel().rows.length > 3 && (
<div className='absolute z-10 transform translate-x-2 -translate-y-2 bg-white border rounded shadow opacity-100 hover:bg-white border-muted-foreground/50 top left rotate-2'>
<Table>
<TableBody>
{table.getRowModel().rows.map(
(row) =>
row.id === activeId && (
<TableRow key={row.id} id={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell
className='p-1 align-top'
key={cell.id}
style={{ width: cell.column.getSize() }}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
)
)}
</TableBody>
</Table>
</div>
)}
</div>
)}
</DragOverlay>,
document.body
)}
</DndContext>
);
}

View File

@ -0,0 +1,212 @@
import {
Button,
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
Popover,
PopoverContent,
PopoverTrigger,
Separator,
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/ui";
import { Table } from "@tanstack/react-table";
import {
CalendarIcon,
CirclePlusIcon,
ClockIcon,
CopyPlusIcon,
ForwardIcon,
MoreVerticalIcon,
ReplyAllIcon,
ReplyIcon,
ScanIcon,
Trash2Icon,
} from "lucide-react";
export const SortableDataTableToolbar = ({
table,
}: {
table: Table<unknown>;
}) => {
const selectedRowsCount = table.getSelectedRowModel().rows.length;
if (selectedRowsCount) {
return (
<div className="flex items-center h-12 p-1 text-white rounded-md bg-primary ">
<div className="flex items-center gap-2">
<Button>{`${selectedRowsCount} filas seleccionadas`}</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
disabled={!table.getSelectedRowModel().rows.length}
onClick={() => table.options.meta?.duplicateItems()}
>
<CopyPlusIcon className="w-4 h-4" />
<span className="sm:sr-only">Duplicar</span>
</Button>
</TooltipTrigger>
<TooltipContent>
Duplica las fila(s) seleccionadas(s)
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
disabled={!table.getSelectedRowModel().rows.length}
onClick={() => table.options.meta?.deleteItems()}
>
<Trash2Icon className="w-4 h-4" />
<span className="sm:sr-only">Eliminar</span>
</Button>
</TooltipTrigger>
<TooltipContent>Elimina las fila(s) seleccionada(s)</TooltipContent>
</Tooltip>
<Separator orientation="vertical" className="h-6 mx-1 bg-muted/50" />
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
disabled={!table.getSelectedRowModel().rows.length}
onClick={() => table.resetRowSelection()}
>
<ScanIcon className="w-4 h-4 md:mr-2" />
<span className="sm:sr-only">Quitar selección</span>
</Button>
</TooltipTrigger>
<TooltipContent>Quita la selección</TooltipContent>
</Tooltip>
</div>
</div>
);
}
return (
<div className="flex items-center h-12 p-1 rounded-md bg-muted/50 text-muted-foreground">
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => table.options.meta?.appendItem()}
>
<CirclePlusIcon className="w-4 h-4" />
<span className="sr-only">Añadir</span>
</Button>
</TooltipTrigger>
<TooltipContent>Añadir fila</TooltipContent>
</Tooltip>
<Separator
orientation="vertical"
className="h-6 mx-1 bg-muted-foreground/30"
/>
<Tooltip>
<Popover>
<PopoverTrigger asChild>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon">
<ClockIcon className="w-4 h-4" />
<span className="sr-only">Snooze</span>
</Button>
</TooltipTrigger>
</PopoverTrigger>
<PopoverContent className="flex w-[535px] p-0">
<div className="flex flex-col gap-2 px-2 py-4 border-r">
<div className="px-4 text-sm font-medium">Snooze until</div>
<div className="grid min-w-[250px] gap-1">
<Button variant="ghost" className="justify-start font-normal">
Later today{" "}
<span className="ml-auto text-muted-foreground"></span>
</Button>
<Button variant="ghost" className="justify-start font-normal">
Tomorrow
<span className="ml-auto text-muted-foreground"></span>
</Button>
<Button variant="ghost" className="justify-start font-normal">
This weekend
<span className="ml-auto text-muted-foreground"></span>
</Button>
<Button variant="ghost" className="justify-start font-normal">
Next week
<span className="ml-auto text-muted-foreground"></span>
</Button>
</div>
</div>
<div className="p-2">
<CalendarIcon />
</div>
</PopoverContent>
</Popover>
<TooltipContent>Snooze</TooltipContent>
</Tooltip>
</div>
<div className="flex items-center gap-2 ml-auto">
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon">
<ReplyIcon className="w-4 h-4" />
<span className="sr-only">Reply</span>
</Button>
</TooltipTrigger>
<TooltipContent>Reply</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon">
<ReplyAllIcon className="w-4 h-4" />
<span className="sr-only">Reply all</span>
</Button>
</TooltipTrigger>
<TooltipContent>Reply all</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon">
<ForwardIcon className="w-4 h-4" />
<span className="sr-only">Forward</span>
</Button>
</TooltipTrigger>
<TooltipContent>Forward</TooltipContent>
</Tooltip>
</div>
<Separator
orientation="vertical"
className="h-6 mx-1 bg-muted-foreground/30"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreVerticalIcon className="w-4 h-4" />
<span className="sr-only">Columnas</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table.getAllColumns().map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
disabled={!column.getCanHide()}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};

View File

@ -0,0 +1,80 @@
import { cn } from "@/lib/utils";
import { TableRow } from "@/ui/table";
import { DraggableSyntheticListeners } from "@dnd-kit/core";
import { defaultAnimateLayoutChanges, useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import {
CSSProperties,
PropsWithChildren,
createContext,
useMemo,
} from "react";
import { SortableProps } from "./SortableDataTable";
interface Context {
attributes: Record<string, any>;
listeners: DraggableSyntheticListeners;
ref(node: HTMLElement | null): void;
}
export const SortableTableRowContext = createContext<Context>({
attributes: {},
listeners: undefined,
ref() {},
});
function animateLayoutChanges(args) {
if (args.isSorting || args.wasDragging) {
return defaultAnimateLayoutChanges(args);
}
return true;
}
export function SortableTableRow({
id,
children,
}: PropsWithChildren<SortableProps>) {
const {
attributes,
isDragging,
listeners,
setNodeRef,
setActivatorNodeRef,
transform,
transition,
} = useSortable({
animateLayoutChanges,
id,
});
const style: CSSProperties = {
transform: CSS.Translate.toString(transform),
transition,
};
const context = useMemo(
() => ({
attributes,
listeners,
ref: setActivatorNodeRef,
}),
[attributes, listeners, setActivatorNodeRef],
);
return (
<SortableTableRowContext.Provider value={context}>
<TableRow
key={id}
id={String(id)}
className={cn(
isDragging ? "opacity-40" : "opacity-100",
"hover:bg-muted/30 m-0",
)}
ref={setNodeRef}
style={style}
>
{children}
</TableRow>
</SortableTableRowContext.Provider>
);
}

View File

@ -0,0 +1 @@
export * from "./SortableDataTable";

View File

@ -1,8 +1,14 @@
export * from "./ButtonGroup";
export * from "./Container";
export * from "./CustomButtons";
export * from "./CustomDialog";
export * from "./DataTable";
export * from "./EmptyState";
export * from "./ErrorOverlay";
export * from "./Forms";
export * from "./Layout";
export * from "./LoadingIndicator";
export * from "./LoadingOverlay";
export * from "./ProtectedRoute";
export * from "./SorteableDataTable";
export * from "./TailwindIndicator";

View File

@ -1,6 +1,6 @@
/*export * from "./useBreadcrumbs";
export * from "./useDataSource";
export * from "./useDataTable";
export * from "./useForm";
export * from "./useLoadingOvertime";
export * from "./useMounted";
@ -13,4 +13,6 @@ export * from "./useUrlId";
export * from "./useAuth";
export * from "./useCustomDialog";
export * from "./useDataTable";
export * from "./usePagination";
export * from "./useTheme";

View File

@ -1,7 +1,7 @@
import { PropsWithChildren, createContext } from "react";
import { IDataSource } from "./DataSource";
export const DataSourceContext = createContext<IDataSource | null>(null);
export const DataSourceContext = createContext<IDataSource | undefined>(undefined);
export const DataSourceProvider = ({
dataSource,

View File

@ -0,0 +1,21 @@
import { usePaginationParams } from "@/lib/hooks";
import { PropsWithChildren, createContext } from "react";
export interface IDataTableContextState {}
export const DataTableContext = createContext<IDataTableContextState | null>(null);
export const DataTableProvider = ({ children }: PropsWithChildren) => {
const [pagination, setPagination] = usePaginationParams();
return (
<DataTableContext.Provider
value={{
pagination,
setPagination,
}}
>
{children}
</DataTableContext.Provider>
);
};

View File

@ -1,3 +1,5 @@
export * from "./DataTableContext";
export * from "./useDataTable";
export * from "./useDataTableColumns";
export * from "./useDataTableContext";
export * from "./useQueryDataTable";

View File

@ -0,0 +1,12 @@
export interface Option {
label: string;
value: string;
icon?: React.ComponentType<{ className?: string }>;
}
export interface DataTableFilterField<TData> {
label: string;
value: keyof TData;
placeholder?: string;
options?: Option[];
}

View File

@ -16,8 +16,8 @@ import {
import { getDataTableSelectionColumn } from "@/components";
import React, { useCallback, useMemo, useState } from "react";
import { useSearchParams } from "react-router-dom";
import { DataTableFilterField } from "../../types";
import { usePaginationParams } from "../usePagination";
import { DataTableFilterField } from "./types";
import { useDataTableContext } from "./useDataTableContext";
//import { useDebounce } from "@/hooks/use-debounce";
@ -110,13 +110,17 @@ export function useDataTable<TData, TValue>({
enableSorting = false,
enableHiding = false,
enableRowSelection = false,
onPaginationChange,
filterFields = [],
// eslint-disable-next-line @typescript-eslint/no-unused-vars
enableAdvancedFilter = false,
}: UseDataTableProps<TData, TValue>) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [searchParams, setSearchParams] = useSearchParams();
const [pagination, setPagination] = usePaginationParams();
const { pagination, setPagination } = useDataTableContext();
console.log("pagination TABLA =>", pagination);
const [sorting, setSorting] = useState<SortingState>([]);
// Memoize computation of searchableColumns and filterableColumns
@ -149,12 +153,8 @@ export function useDataTable<TData, TValue>({
const initialColumnFilters: ColumnFiltersState = useMemo(() => {
return Array.from(searchParams.entries()).reduce<ColumnFiltersState>(
(filters, [key, value]) => {
const filterableColumn = filterableColumns.find(
(column) => column.value === key
);
const searchableColumn = searchableColumns.find(
(column) => column.value === key
);
const filterableColumn = filterableColumns.find((column) => column.value === key);
const searchableColumn = searchableColumns.find((column) => column.value === key);
if (filterableColumn) {
filters.push({
@ -177,8 +177,7 @@ export function useDataTable<TData, TValue>({
// Table states
const [rowSelection, setRowSelection] = React.useState({});
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
const [columnFilters, setColumnFilters] =
React.useState<ColumnFiltersState>(initialColumnFilters);
@ -186,7 +185,6 @@ export function useDataTable<TData, TValue>({
const paginationUpdater: OnChangeFn<PaginationState> = (updater) => {
if (typeof updater === "function") {
const newPagination = updater(pagination);
console.log(newPagination);
setPagination(newPagination);
}
};

View File

@ -0,0 +1,9 @@
import { useContext } from "react";
import { DataTableContext } from "./DataTableContext";
export const useDataTableContext = () => {
const context = useContext(DataTableContext);
if (context === null)
throw new Error("useDataTableContext must be used within a DataTableProvider");
return context;
};

View File

@ -0,0 +1,75 @@
import { act, renderHook } from "@testing-library/react-hooks";
import {
INITIAL_PAGE_INDEX,
INITIAL_PAGE_SIZE,
MAX_PAGE_SIZE,
MIN_PAGE_SIZE,
PaginationState,
usePagination,
} from "./usePagination";
describe("usePagination", () => {
it("should initialize with default values", () => {
const { result } = renderHook(() => usePagination());
const [pagination] = result.current;
expect(pagination.pageIndex).toBe(INITIAL_PAGE_INDEX);
expect(pagination.pageSize).toBe(INITIAL_PAGE_SIZE);
});
it("should initialize with custom values", () => {
const customPageIndex = 2;
const customPageSize = 20;
const { result } = renderHook(() => usePagination(customPageIndex, customPageSize));
const [pagination] = result.current;
expect(pagination.pageIndex).toBe(customPageIndex);
expect(pagination.pageSize).toBe(customPageSize);
});
it("should update pagination state correctly", () => {
const { result } = renderHook(() => usePagination());
const [pagination, updatePagination] = result.current;
const newPagination: PaginationState = { pageIndex: 1, pageSize: 25 };
act(() => {
updatePagination(newPagination);
});
expect(pagination.pageIndex).toBe(newPagination.pageIndex);
expect(pagination.pageSize).toBe(newPagination.pageSize);
});
it("should not update pageIndex below INITIAL_PAGE_INDEX", () => {
const { result } = renderHook(() => usePagination());
const [, updatePagination] = result.current;
act(() => {
updatePagination({ pageIndex: -1, pageSize: INITIAL_PAGE_SIZE });
});
const [pagination] = result.current;
expect(pagination.pageIndex).toBe(INITIAL_PAGE_INDEX);
});
it("should not update pageSize below MIN_PAGE_SIZE or above MAX_PAGE_SIZE", () => {
const { result } = renderHook(() => usePagination());
const [, updatePagination] = result.current;
act(() => {
updatePagination({ pageIndex: INITIAL_PAGE_INDEX, pageSize: MIN_PAGE_SIZE - 1 });
});
let [pagination] = result.current;
expect(pagination.pageSize).toBe(INITIAL_PAGE_SIZE); // No cambia porque pageSize es menor que el mínimo
act(() => {
updatePagination({ pageIndex: INITIAL_PAGE_INDEX, pageSize: MAX_PAGE_SIZE + 1 });
});
[pagination] = result.current;
expect(pagination.pageSize).toBe(INITIAL_PAGE_SIZE); // No cambia porque pageSize es mayor que el máximo
});
});

View File

@ -6,7 +6,7 @@ export const INITIAL_PAGE_SIZE = 10;
export const MIN_PAGE_INDEX = 0;
export const MIN_PAGE_SIZE = 1;
export const MAX_PAGE_SIZE = Number.MAX_SAFE_INTEGER;
export const MAX_PAGE_SIZE = 100; //Number.MAX_SAFE_INTEGER;
export const DEFAULT_PAGE_SIZES = [10, 25, 50, 100];
@ -31,19 +31,23 @@ export const usePagination = (
const updatePagination = (newPagination: PaginationState) => {
// Realiza comprobaciones antes de actualizar el estado
const validatedPagination = newPagination;
if (newPagination.pageIndex < INITIAL_PAGE_INDEX) {
newPagination.pageIndex = INITIAL_PAGE_INDEX;
if (validatedPagination.pageIndex < INITIAL_PAGE_INDEX) {
validatedPagination.pageIndex = INITIAL_PAGE_INDEX;
}
if (
newPagination.pageSize < MIN_PAGE_SIZE ||
newPagination.pageSize > MAX_PAGE_SIZE
) {
return;
if (newPagination.pageSize < MIN_PAGE_SIZE || newPagination.pageSize > MAX_PAGE_SIZE) {
validatedPagination.pageSize = MIN_PAGE_SIZE;
}
setPagination(newPagination);
setPagination((oldPagination) => ({
...oldPagination,
pageIndex: newPagination.pageIndex,
pageSize: newPagination.pageSize,
}));
return validatedPagination;
};
return [pagination, updatePagination] as const;

View File

@ -1,8 +1,11 @@
import { useEffect, useMemo } from "react";
import { PaginationState } from "@tanstack/react-table";
import { useMemo } from "react";
import { useSearchParams } from "react-router-dom";
import {
INITIAL_PAGE_INDEX,
INITIAL_PAGE_SIZE,
MAX_PAGE_SIZE,
MIN_PAGE_SIZE,
usePagination,
} from "./usePagination";
@ -17,24 +20,29 @@ export const usePaginationParams = (
const calculatedPageIndex = useMemo(() => {
const parsedPageIndex = parseInt(urlParamPageIndex ?? "", 10);
return !isNaN(parsedPageIndex) ? parsedPageIndex : initialPageIndex;
let result = !isNaN(parsedPageIndex) ? parsedPageIndex : initialPageIndex;
if (result < initialPageIndex) {
result = initialPageIndex;
}
return result;
}, [urlParamPageIndex, initialPageIndex]);
const calculatedPageSize = useMemo(() => {
const parsedPageSize = parseInt(urlParamPageSize ?? "", 10);
return !isNaN(parsedPageSize) ? parsedPageSize : initialPageSize;
let result = !isNaN(parsedPageSize) ? parsedPageSize : initialPageSize;
if (result < MIN_PAGE_SIZE || result > MAX_PAGE_SIZE) {
result = initialPageSize;
}
return result;
}, [urlParamPageSize, initialPageSize]);
const [pagination, setPagination] = usePagination(
calculatedPageIndex,
calculatedPageSize
);
const [pagination, setPagination] = usePagination(calculatedPageIndex, calculatedPageSize);
useEffect(() => {
/*useEffect(() => {
// Actualizar la URL cuando cambia la paginación
const actualSearchParam = Object.fromEntries(
new URLSearchParams(urlSearchParams)
);
const actualSearchParam = Object.fromEntries(new URLSearchParams(urlSearchParams));
if (
String(pagination.pageIndex) !== actualSearchParam.page_index ||
@ -46,7 +54,17 @@ export const usePaginationParams = (
page_size: String(pagination.pageSize),
});
}
}, [pagination]);
}, [pagination]);*/
return [pagination, setPagination] as const;
const updatePagination = (newPagination: PaginationState) => {
const _validatedPagination = setPagination(newPagination);
setUrlSearchParams({
//...actualSearchParam,
page_index: String(_validatedPagination.pageIndex),
page_size: String(_validatedPagination.pageSize),
});
};
return [pagination, updatePagination];
};

View File

@ -1,14 +1,36 @@
{
"translation": {
"title": "Presupuestador para distribuidores",
"common": {
"cancel": "Cancel",
"no": "No",
"yes": "Yes",
"Accept": "Accept"
},
"main_menu": {
"home": "Home",
"settings": "Settings",
"dealers": "Dealers",
"catalog": "Catalog",
"quotes": "Quotes",
"search_placeholder": "Search product, quotes, etc...",
"user": {
"user_menu": "User menu",
"my_account": "My account",
"profile": "Profile",
"settings": "Settings",
"support": "Support",
"logout": "Logout"
},
"logout": {}
},
"login_page": {
"title": "Presupuestador para distribuidores",
"description": "Introduzca su dirección de correo electrónico y contraseña para acceder",
"title": "Uecko Quotes",
"description": "Enter your email address and password to login",
"email_label": "Email",
"email_placeholder": "micorreo@ejemplo.com",
"email_placeholder": "user@sample.com",
"password_label": "Password",
"forgotten_password": "¿Has olvidado tu contraseña?",
"become_dealer": "¿Quieres ser distribuidor de Uecko?",
"forgotten_password": "Forgot your password?",
"become_dealer": "Do you want to become a Uecko dealer?",
"contact_us": "Contact us",
"login": "Login",
"error": "Error"

View File

@ -12,7 +12,7 @@
"dealers": "Distribuidores",
"catalog": "Catálogo",
"quotes": "Cotizaciones",
"search_placeholder": "Buscar productos, distribuidores, etc...",
"search_placeholder": "Buscar productos, cotizaciones, etc...",
"user": {
"user_menu": "Menú del usuario",
"my_account": "Mi cuenta",
@ -34,6 +34,12 @@
"contact_us": "Contacta con nosotros",
"login": "Entrar",
"error": "Error"
},
"dashboard": {
"welcome": "Bienvenido"
},
"catalog": {
"title": "Catálogo de artículos"
}
}
}