Skip to content

Commit 66f2fb8

Browse files
committed
feat: setup table list event components, and create page events
1 parent e5bb175 commit 66f2fb8

15 files changed

Lines changed: 603 additions & 54 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"@radix-ui/react-separator": "^1.1.7",
3131
"@radix-ui/react-slot": "^1.2.3",
3232
"@radix-ui/react-tooltip": "^1.2.7",
33+
"@tanstack/react-table": "^8.21.3",
3334
"@tiptap/extension-image": "^3.2.0",
3435
"@tiptap/extension-link": "^3.2.0",
3536
"@tiptap/extension-placeholder": "^3.2.0",

pnpm-lock.yaml

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/[locale]/admin/events/[eventId]/edit/page.tsx

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"use client";
2+
3+
const EventDetail = () => {
4+
return <div></div>;
5+
};
6+
7+
export default EventDetail;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"use client";
2+
3+
import { AdminEventsCreatePage } from "@/features/events/admin";
4+
5+
const CreateEventPage = () => {
6+
return <AdminEventsCreatePage />;
7+
};
8+
9+
export default CreateEventPage;
Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,9 @@
11
"use client";
22

3-
import TextEditor from "@/components/common/TextEditor";
4-
import { useEvents } from "@/features/events/hooks/useEvent";
5-
import Link from "next/link";
3+
import { AdminEventsListPage } from "@/features/events/admin";
64

75
const EventListPage = () => {
8-
const { events, isLoading } = useEvents();
9-
return (
10-
<div>
11-
<h1>Event List</h1>
12-
<TextEditor />
13-
{isLoading && <p>Fetching events...</p>}
14-
<ul>
15-
{events.map((ev) => (
16-
<li key={ev.id}>
17-
{ev.title} <Link href={`/admin/events/${ev.id}/edit`}>Edit</Link>
18-
</li>
19-
))}
20-
</ul>
21-
</div>
22-
);
6+
return <AdminEventsListPage />;
237
};
248

259
export default EventListPage;
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"use client";
2+
3+
import { useState, useMemo } from "react";
4+
import { useReactTable, getCoreRowModel, flexRender, ColumnDef } from "@tanstack/react-table";
5+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/Table";
6+
import { TablePagination } from "./TablePagination";
7+
import { TableToolbar } from "./TableToolbar";
8+
9+
interface TableDataProps<T> {
10+
data: T[];
11+
columns: ColumnDef<T>[];
12+
searchable?: boolean;
13+
searchPlaceholder?: string;
14+
itemsPerPage?: number;
15+
className?: string;
16+
rightAction?: React.ReactNode;
17+
}
18+
19+
function TableData<T>({
20+
data,
21+
columns,
22+
searchable = false,
23+
searchPlaceholder = "Search...",
24+
itemsPerPage = 5,
25+
className,
26+
rightAction,
27+
}: TableDataProps<T>) {
28+
const [globalFilter, setGlobalFilter] = useState("");
29+
const [currentPage, setCurrentPage] = useState(1);
30+
31+
const filteredData = useMemo(() => {
32+
if (!searchable || !globalFilter) return data;
33+
34+
return data.filter((item: T) =>
35+
Object.values(item as Record<string, unknown>).some((value) =>
36+
value?.toString().toLowerCase().includes(globalFilter.toLowerCase())
37+
)
38+
);
39+
}, [data, globalFilter, searchable]);
40+
41+
const totalPages = Math.ceil(filteredData.length / itemsPerPage);
42+
43+
const paginatedData = useMemo(() => {
44+
const startIndex = (currentPage - 1) * itemsPerPage;
45+
return filteredData.slice(startIndex, startIndex + itemsPerPage);
46+
}, [filteredData, currentPage, itemsPerPage]);
47+
48+
const table = useReactTable({
49+
data: paginatedData,
50+
columns,
51+
getCoreRowModel: getCoreRowModel(),
52+
manualPagination: true,
53+
});
54+
55+
return (
56+
<div className={`space-y-4 ${className}`}>
57+
<TableToolbar
58+
searchable={searchable}
59+
searchPlaceholder={searchPlaceholder}
60+
searchValue={globalFilter}
61+
onSearchChange={(value) => {
62+
setGlobalFilter(value);
63+
setCurrentPage(1);
64+
}}
65+
rightAction={rightAction}
66+
/>
67+
68+
<div className="rounded-md border">
69+
<Table>
70+
<TableHeader className="bg-muted/50">
71+
{table.getHeaderGroups().map((headerGroup) => (
72+
<TableRow key={headerGroup.id}>
73+
{headerGroup.headers.map((header) => (
74+
<TableHead key={header.id} className="text-foreground font-semibold">
75+
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
76+
</TableHead>
77+
))}
78+
</TableRow>
79+
))}
80+
</TableHeader>
81+
<TableBody>
82+
{table.getRowModel().rows.length === 0 ? (
83+
<TableRow>
84+
<TableCell colSpan={columns.length} className="text-muted-foreground h-24 text-center">
85+
No data found.
86+
</TableCell>
87+
</TableRow>
88+
) : (
89+
table.getRowModel().rows.map((row) => (
90+
<TableRow key={row.id}>
91+
{row.getVisibleCells().map((cell) => (
92+
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
93+
))}
94+
</TableRow>
95+
))
96+
)}
97+
</TableBody>
98+
</Table>
99+
</div>
100+
101+
{totalPages > 1 && (
102+
<div className="flex items-center justify-between">
103+
<div className="text-muted-foreground text-sm">
104+
{currentPage} of {totalPages} pages
105+
</div>
106+
107+
<TablePagination currentPage={currentPage} totalPages={totalPages} onPageChange={setCurrentPage} />
108+
</div>
109+
)}
110+
</div>
111+
);
112+
}
113+
114+
export default TableData;
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"use client";
2+
import React from "react";
3+
import {
4+
Pagination as UIPagination,
5+
PaginationContent,
6+
PaginationItem,
7+
PaginationLink,
8+
PaginationNext,
9+
PaginationPrevious,
10+
PaginationEllipsis,
11+
} from "@/components/ui/Pagination";
12+
import { buttonVariants } from "@/components/ui/Button";
13+
import { cn } from "@/lib/utils";
14+
import { usePagination } from "@/components/hooks/UsePagination";
15+
16+
interface TablePaginationProps {
17+
currentPage: number;
18+
totalPages: number;
19+
onPageChange: (page: number) => void;
20+
className?: string;
21+
}
22+
23+
export const TablePagination: React.FC<TablePaginationProps> = ({
24+
currentPage,
25+
totalPages,
26+
onPageChange,
27+
className,
28+
}) => {
29+
const { hasNextPage, hasPrevPage, nextPage, prevPage, pages } = usePagination({
30+
currentPage,
31+
totalPages,
32+
});
33+
34+
if (totalPages <= 1) {
35+
return null;
36+
}
37+
38+
return (
39+
<div className={cn("", className)}>
40+
<UIPagination>
41+
<PaginationContent>
42+
<PaginationItem>
43+
<PaginationPrevious
44+
onClick={() => hasPrevPage && onPageChange(prevPage!)}
45+
className={cn(
46+
"bg-secondary text-secondary-foreground cursor-pointer",
47+
!hasPrevPage && "cursor-not-allowed opacity-50"
48+
)}
49+
size="icon"
50+
/>
51+
</PaginationItem>
52+
53+
{pages.map((page, index) => {
54+
if (page === "ellipsis") {
55+
return (
56+
<PaginationItem key={index}>
57+
<PaginationEllipsis />
58+
</PaginationItem>
59+
);
60+
}
61+
62+
const isActive = page === currentPage;
63+
64+
return (
65+
<PaginationItem key={page}>
66+
<PaginationLink
67+
onClick={() => onPageChange(page as number)}
68+
isActive={isActive}
69+
className={cn("cursor-pointer", {
70+
[buttonVariants({
71+
variant: "default",
72+
className: "bg-hmc-base-blue hover:bg-hmc-base-blue text-white !shadow-none hover:text-white",
73+
})]: isActive,
74+
"bg-secondary text-secondary-foreground": !isActive,
75+
})}
76+
>
77+
{page}
78+
</PaginationLink>
79+
</PaginationItem>
80+
);
81+
})}
82+
83+
<PaginationItem>
84+
<PaginationNext
85+
onClick={() => hasNextPage && onPageChange(nextPage!)}
86+
className={cn(
87+
"bg-secondary text-secondary-foreground cursor-pointer",
88+
!hasNextPage && "cursor-not-allowed opacity-50"
89+
)}
90+
size="icon"
91+
/>
92+
</PaginationItem>
93+
</PaginationContent>
94+
</UIPagination>
95+
</div>
96+
);
97+
};
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"use client";
2+
3+
import { Input } from "@/components/ui/Input";
4+
import { Search } from "lucide-react";
5+
6+
interface TableToolbarProps {
7+
searchable?: boolean;
8+
searchPlaceholder?: string;
9+
searchValue?: string;
10+
onSearchChange?: (value: string) => void;
11+
rightAction?: React.ReactNode;
12+
}
13+
14+
export const TableToolbar = ({
15+
searchable = false,
16+
searchPlaceholder = "Search...",
17+
searchValue = "",
18+
onSearchChange,
19+
rightAction,
20+
}: TableToolbarProps) => {
21+
return (
22+
<div className="flex items-center justify-between gap-4">
23+
{/* Search Section */}
24+
<div className="flex-1">
25+
{searchable && (
26+
<div className="relative max-w-sm">
27+
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
28+
<Input
29+
placeholder={searchPlaceholder}
30+
value={searchValue}
31+
onChange={(e) => onSearchChange?.(e.target.value)}
32+
className="pl-10"
33+
/>
34+
</div>
35+
)}
36+
</div>
37+
38+
{/* Right Action Section */}
39+
{rightAction && <div className="flex-shrink-0">{rightAction}</div>}
40+
</div>
41+
);
42+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { default } from "./TableData";
2+
export { TableToolbar } from "./TableToolbar";
3+
export { TablePagination } from "./TablePagination";

0 commit comments

Comments
 (0)