-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(client): added posts list page as well as routing
- Loading branch information
1 parent
84b4fe3
commit e83a788
Showing
6 changed files
with
432 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./posts"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) => { | ||
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> | ||
), | ||
}, | ||
]; | ||
}; |
Oops, something went wrong.