Skip to content

Commit

Permalink
feat(client): added posts list page as well as routing
Browse files Browse the repository at this point in the history
  • Loading branch information
EchoSkorJjj committed Jan 3, 2024
1 parent 84b4fe3 commit e83a788
Show file tree
Hide file tree
Showing 6 changed files with 432 additions and 0 deletions.
24 changes: 24 additions & 0 deletions client/src/features/interfaces/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Column } from "@tanstack/react-table";

export interface ICategory {
id: number;
title: string;
}

export interface IPost {
id: number;
title: string;
content: string;
status: "published" | "draft" | "rejected";
category: { id: number };
createdAt: string;
}

export interface ColumnButtonProps {
column: Column<any, any>; // eslint-disable-line
}

export interface FilterElementProps {
value: any; // eslint-disable-line
onChange: (value: any) => void; // eslint-disable-line
}
1 change: 1 addition & 0 deletions client/src/features/pages/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./posts";
151 changes: 151 additions & 0 deletions client/src/features/pages/posts/List.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import React, { useEffect, useState } from "react";
import { CgAddR } from "react-icons/cg";
import {
Box,
Button,
Flex,
Heading,
HStack,
Spinner,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr,
} from "@chakra-ui/react";
import {
ColumnDef,
flexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";

import makeData from "~features/components/data/MakeData";
import { Pagination } from "~features/components/pagination";
import { ColumnFilter, ColumnSorter } from "~features/components/table";
import { IPost } from "~features/interfaces";

import { usePostColumns } from "./index";

export const PostList: React.FC = () => {
const [isLoading, setIsLoading] = useState(true);
const [posts, setPosts] = useState<IPost[]>([]);

const columns: ColumnDef<IPost>[] = usePostColumns();

useEffect(() => {
const fetchAllPosts = async () => {
// Replace the following with actual API call
const data: IPost[] = makeData(1000);
setPosts(data);
setIsLoading(false);
};
fetchAllPosts();
}, []);

const table = useReactTable({
data: posts,
columns,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
debugTable: false,
debugHeaders: false,
debugColumns: false,
});

if (isLoading) {
return <Spinner />;
}

return (
<Box p="4" bg="white">
<Flex justifyContent="space-between" m={4} alignItems="center">
<Flex alignItems="center">
<Heading as="h3" size="lg">
Posts
</Heading>
</Flex>
<Flex>
<Button
as="a"
href="/posts/create"
leftIcon={<CgAddR />}
colorScheme="blue"
>
Create
</Button>
</Flex>
</Flex>
<TableContainer>
<Table variant="simple" whiteSpace="pre-line">
<Thead>
{table.getHeaderGroups().map((headerGroup) => (
<Tr key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<Th key={header.id}>
{!header.isPlaceholder && (
<HStack spacing="xs">
<Box>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</Box>
<HStack spacing="xs">
<ColumnSorter column={header.column} />
<ColumnFilter column={header.column} />
</HStack>
</HStack>
)}
</Th>
);
})}
</Tr>
))}
</Thead>
<Tbody>
{table.getRowModel().rows.map((row) => {
return (
<Tr key={row.id}>
{row.getVisibleCells().map((cell) => {
return (
<Td key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</Td>
);
})}
</Tr>
);
})}
</Tbody>
</Table>
</TableContainer>
<Pagination
canPreviousPage={table.getCanPreviousPage()}
canNextPage={table.getCanNextPage()}
pageCount={table.getPageCount()}
pageIndex={table.getState().pagination.pageIndex}
pageSize={table.getState().pagination.pageSize}
setPageIndex={table.setPageIndex}
nextPage={table.nextPage}
previousPage={table.previousPage}
setPageSize={table.setPageSize}
/>
</Box>
);
};
211 changes: 211 additions & 0 deletions client/src/features/pages/posts/PostColumns.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { DeleteIcon, EditIcon, ViewIcon } from "@chakra-ui/icons";
import {
Button,
ButtonGroup,
HStack,
IconButton,
Popover,
PopoverArrow,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverFooter,
PopoverHeader,
PopoverTrigger,
Select,
useToast,
} from "@chakra-ui/react";
import { ColumnDef, FilterFn } from "@tanstack/react-table";

import { getAllCategories } from "~features/components/data";
import { FilterElementProps, ICategory, IPost } from "~features/interfaces";

export const usePostColumns = (): ColumnDef<IPost>[] => {
const navigate = useNavigate();
const toast = useToast();
const [activePopoverId, setActivePopoverId] = useState<number | null>(null);
const [categories, setCategories] = useState<ICategory[]>([]);
const handleEdit = (id: number) => {
navigate(`/posts/edit/${id}`);
};

const handleView = (id: number) => {
navigate(`/posts/show/${id}`);
};

const handleDelete = (id: number) => {
setActivePopoverId(id);
};

const onDelete = () => {
// actual deletion logic here
toast({ title: `Post deleted ${activePopoverId}`, status: "error" });
setActivePopoverId(null);
};

const customCategoryFilter: FilterFn<any> = (row, columnId, filterValue) => {

Check warning on line 48 in client/src/features/pages/posts/PostColumns.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
const rowValue = Number(row.getValue(columnId));
const filterNumber = Number(filterValue);
return rowValue === filterNumber;
};

// Mock function to simulate loading categories - replace with actual API call
useEffect(() => {
// Simulate fetching categories data
const fetchAllCategories = async () => {
// Replace with actual API call

setCategories(getAllCategories);
};
fetchAllCategories();
}, []);

return [
{
id: "id",
accessorKey: "id",
header: "ID",
enableColumnFilter: false,
},
{
id: "title",
accessorKey: "title",
header: "Title",
meta: {
filterOperator: "contains",
},
},
{
id: "status",
accessorKey: "status",
header: "Status",
meta: {
filterElement: function render(props: FilterElementProps) {
return (
<Select
borderRadius="md"
size="sm"
placeholder="All Status"
{...props}
>
<option value="published">published</option>
<option value="draft">draft</option>
<option value="rejected">rejected</option>
</Select>
);
},
filterOperator: "eq",
},
},
{
id: "category.id",
accessorKey: "category.id",
header: "Category",
meta: {
filterElement: function render(props: FilterElementProps) {
return (
<Select
borderRadius="md"
size="sm"
placeholder="All Category"
{...props}
>
{categories.map((category) => (
<option key={category.id} value={category.id}>
{category.title}
</option>
))}
</Select>
);
},
filterOperator: "eq",
},
filterFn: customCategoryFilter,
cell: function render({ cell: { getValue } }) {
const categoryId = getValue() as number;
const categoryObject = getAllCategories.find(
(category) => category.id === categoryId,
);

return categoryObject ? categoryObject.title : "Unknown Category";
},
},
{
id: "createdAt",
accessorKey: "createdAt",
header: "Created At",
enableColumnFilter: false,
cell: (info) => {
const dateValue = info.getValue() as string;

if (!dateValue || isNaN(new Date(dateValue).getTime())) {
return "Invalid date";
}

return dateValue;
},
},
{
id: "actions",
header: "Actions",
accessorKey: "id",
enableColumnFilter: false,
enableSorting: false,
cell: (info) => (
<HStack>
<IconButton
aria-label="View"
icon={<ViewIcon />}
onClick={() => handleView(info.row.original.id)}
variant="outline"
/>
<IconButton
aria-label="Edit"
icon={<EditIcon />}
onClick={() => handleEdit(info.row.original.id)}
variant="outline"
/>

<Popover
returnFocusOnClose={false}
isOpen={activePopoverId === info.row.original.id}
onClose={() => setActivePopoverId(null)}
placement="bottom"
closeOnBlur={false}
>
<PopoverTrigger>
<IconButton
aria-label="Delete"
icon={<DeleteIcon />}
onClick={() => handleDelete(info.row.original.id)}
variant="outline"
colorScheme="red"
/>
</PopoverTrigger>
<PopoverContent>
<PopoverHeader fontWeight="semibold">Confirmation</PopoverHeader>
<PopoverArrow />
<PopoverCloseButton />
<PopoverBody>Are you sure?</PopoverBody>
<PopoverFooter display="flex" justifyContent="flex-end">
<ButtonGroup size="sm">
<Button
variant="outline"
onClick={() => setActivePopoverId(null)}
>
Cancel
</Button>
<Button colorScheme="red" onClick={onDelete}>
Apply
</Button>
</ButtonGroup>
</PopoverFooter>
</PopoverContent>
</Popover>
</HStack>
),
},
];
};
Loading

0 comments on commit e83a788

Please sign in to comment.