add cards table and pagination

This commit is contained in:
2023-10-07 23:44:20 +02:00
parent 450d664f34
commit 33f3fc6137
21 changed files with 635 additions and 52 deletions

View File

@@ -0,0 +1,47 @@
import { Column, Table, TableBody, TableCell, TableHeader, TableRow } from '@/components'
import { Card } from '@/services/decks'
import { formatDate } from '@/utils'
const columns: Column[] = [
{
key: 'question',
title: 'Question',
sortable: true,
},
{
key: 'answer',
title: 'Answer',
sortable: true,
},
{
key: 'updated',
title: 'Last Updated',
sortable: true,
},
{
key: 'grade',
title: 'Grade',
sortable: true,
},
]
type Props = {
cards: Card[] | undefined
}
export const CardsTable = ({ cards }: Props) => {
return (
<Table>
<TableHeader columns={columns} />
<TableBody>
{cards?.map(card => (
<TableRow key={card.id}>
<TableCell>{card.question}</TableCell>
<TableCell>{card.answer}</TableCell>
<TableCell>{formatDate(card.updated)}</TableCell>
<TableCell>{card.grade}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}

View File

@@ -0,0 +1,62 @@
import { Link } from 'react-router-dom'
import {
Column,
Table,
TableBody,
TableCell,
TableHeader,
TableRow,
Typography,
} from '@/components'
import { Deck } from '@/services/decks'
import { formatDate } from '@/utils'
const columns: Column[] = [
{
key: 'name',
title: 'Name',
},
{
key: 'cardsCount',
title: 'Cards',
},
{
key: 'updated',
title: 'Last Updated',
},
{
key: 'author',
title: 'Created By',
},
{
key: 'actions',
title: '',
},
]
type Props = {
decks: Deck[] | undefined
}
export const DecksTable = ({ decks }: Props) => {
return (
<Table>
<TableHeader columns={columns} />
<TableBody>
{decks?.map(deck => (
<TableRow key={deck.id}>
<TableCell>
<Typography variant={'body2'} as={Link} to={`/decks/${deck.id}`}>
{deck.name}
</Typography>
</TableCell>
<TableCell>{deck.cardsCount}</TableCell>
<TableCell>{formatDate(deck.updated)}</TableCell>
<TableCell>{deck.author.name}</TableCell>
<TableCell>...</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}

View File

@@ -0,0 +1,2 @@
export * from './decks-table.tsx'
export * from './cards-table.tsx'

View File

@@ -1,3 +1,4 @@
export * from './ui'
export * from './auth'
export * from './profile'
export * from './decks'

View File

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

View File

@@ -0,0 +1,75 @@
.root {
display: flex;
gap: 25px;
align-items: center;
}
.container {
display: flex;
gap: 12px;
list-style-type: none;
}
@mixin item {
all: unset;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
color: var(--color-light-100);
border-radius: 2px;
}
.item {
@include item;
cursor: pointer;
&:focus-visible {
outline: var(--outline-focus);
}
&:disabled {
cursor: initial;
opacity: 1;
}
&:hover:not(&:disabled) {
background-color: var(--color-dark-500);
}
&.selected {
color: var(--color-dark-900);
background-color: var(--color-light-100);
}
}
.dots {
@include item;
}
.icon {
.item:disabled & {
// important because icons have style prop
color: var(--color-action-disabled) !important;
}
}
.selectBox {
display: flex;
gap: 12px;
align-items: center;
color: var(--color-light-100);
white-space: nowrap;
}
.select {
flex-shrink: 0;
width: 50px;
}

View File

@@ -0,0 +1,192 @@
import { FC } from 'react'
import { clsx } from 'clsx'
import s from './pagination.module.scss'
import { usePagination } from './usePagination'
import { KeyboardArrowLeft, KeyboardArrowRight } from '@/assets'
type PaginationConditionals =
| {
perPage?: null
perPageOptions?: never
onPerPageChange?: never
}
| {
perPage: number
perPageOptions: number[]
onPerPageChange: (itemPerPage: number) => void
}
export type PaginationProps = {
count: number
page: number
onChange: (page: number) => void
siblings?: number
perPage?: number
perPageOptions?: number[]
onPerPageChange?: (itemPerPage: number) => void
} & PaginationConditionals
const classNames = {
root: s.root,
container: s.container,
selectBox: s.selectBox,
select: s.select,
item: s.item,
dots: s.dots,
icon: s.icon,
pageButton(selected?: boolean) {
return clsx(this.item, selected && s.selected)
},
}
export const Pagination: FC<PaginationProps> = ({
onChange,
count,
page,
perPage = null,
perPageOptions,
onPerPageChange,
siblings,
}) => {
const {
paginationRange,
isLastPage,
isFirstPage,
handlePreviousPageClicked,
handleNextPageClicked,
handleMainPageClicked,
} = usePagination({
page,
count,
onChange,
siblings,
})
const showPerPageSelect = !!perPage && !!perPageOptions && !!onPerPageChange
return (
<div className={classNames.root}>
<div className={classNames.container}>
<PrevButton onClick={handlePreviousPageClicked} disabled={isFirstPage} />
<MainPaginationButtons
currentPage={page}
onClick={handleMainPageClicked}
paginationRange={paginationRange}
/>
<NextButton onClick={handleNextPageClicked} disabled={isLastPage} />
</div>
{showPerPageSelect && (
<PerPageSelect
{...{
perPage,
perPageOptions,
onPerPageChange,
}}
/>
)}
</div>
)
}
type NavigationButtonProps = {
onClick: () => void
disabled?: boolean
}
type PageButtonProps = NavigationButtonProps & {
page: number
selected: boolean
}
const Dots: FC = () => {
return <span className={classNames.dots}>&#8230;</span>
}
const PageButton: FC<PageButtonProps> = ({ onClick, disabled, selected, page }) => {
return (
<button
onClick={onClick}
disabled={selected || disabled}
className={classNames.pageButton(selected)}
>
{page}
</button>
)
}
const PrevButton: FC<NavigationButtonProps> = ({ onClick, disabled }) => {
return (
<button className={classNames.item} onClick={onClick} disabled={disabled}>
<KeyboardArrowLeft className={classNames.icon} />
</button>
)
}
const NextButton: FC<NavigationButtonProps> = ({ onClick, disabled }) => {
return (
<button className={classNames.item} onClick={onClick} disabled={disabled}>
<KeyboardArrowRight className={classNames.icon} />
</button>
)
}
type MainPaginationButtonsProps = {
paginationRange: (number | string)[]
currentPage: number
onClick: (pageNumber: number) => () => void
}
const MainPaginationButtons: FC<MainPaginationButtonsProps> = ({
paginationRange,
currentPage,
onClick,
}) => {
return (
<>
{paginationRange.map((page: number | string, index) => {
const isSelected = page === currentPage
if (typeof page !== 'number') {
return <Dots key={index} />
}
return <PageButton key={index} page={page} selected={isSelected} onClick={onClick(page)} />
})}
</>
)
}
export type PerPageSelectProps = {
perPage: number
perPageOptions: number[]
onPerPageChange: (itemPerPage: number) => void
}
export const PerPageSelect: FC<PerPageSelectProps> = ({
perPage,
perPageOptions,
onPerPageChange,
}) => {
const selectOptions = perPageOptions.map(value => ({
label: value,
value,
}))
return (
<div className={classNames.selectBox}>
Показать
{/*<Select*/}
{/* className={classNames.select}*/}
{/* value={perPage}*/}
{/* options={selectOptions}*/}
{/* onChange={onPerPageChange}*/}
{/* variant="pagination"*/}
{/*/>*/}
на странице
</div>
)
}

View File

@@ -0,0 +1,112 @@
import { useCallback, useMemo } from 'react'
// original code: https://www.freecodecamp.org/news/build-a-custom-pagination-component-in-react/
const range = (start: number, end: number) => {
let length = end - start + 1
/*
Create an array of certain length and set the elements within it from
start value to end value.
*/
return Array.from({ length }, (_, idx) => idx + start)
}
const DOTS = '...'
type UsePaginationParamType = {
count: number
siblings?: number
page: number
onChange: (pageNumber: number) => void
}
type PaginationRange = (number | '...')[]
export const usePagination = ({ count, siblings = 1, page, onChange }: UsePaginationParamType) => {
const paginationRange = useMemo(() => {
// Pages count is determined as siblingCount + firstPage + lastPage + page + 2*DOTS
const totalPageNumbers = siblings + 5
/*
Case 1:
If the number of pages is less than the page numbers we want to show in our
paginationComponent, we return the range [1..totalPageCount]
*/
if (totalPageNumbers >= count) {
return range(1, count)
}
/*
Calculate left and right sibling index and make sure they are within range 1 and totalPageCount
*/
const leftSiblingIndex = Math.max(page - siblings, 1)
const rightSiblingIndex = Math.min(page + siblings, count)
/*
We do not show dots when there is only one page number to be inserted
between the extremes of siblings and the page limits i.e 1 and totalPageCount.
Hence, we are using leftSiblingIndex > 2 and rightSiblingIndex < totalPageCount - 2
*/
const shouldShowLeftDots = leftSiblingIndex > 2
const shouldShowRightDots = rightSiblingIndex < count - 2
const firstPageIndex = 1
const lastPageIndex = count
/*
Case 2: No left dots to show, but rights dots to be shown
*/
if (!shouldShowLeftDots && shouldShowRightDots) {
let leftItemCount = 3 + 2 * siblings
let leftRange = range(1, leftItemCount)
return [...leftRange, DOTS, count]
}
/*
Case 3: No right dots to show, but left dots to be shown
*/
if (shouldShowLeftDots && !shouldShowRightDots) {
let rightItemCount = 3 + 2 * siblings
let rightRange = range(count - rightItemCount + 1, count)
return [firstPageIndex, DOTS, ...rightRange]
}
/*
Case 4: Both left and right dots to be shown
*/
if (shouldShowLeftDots && shouldShowRightDots) {
let middleRange = range(leftSiblingIndex, rightSiblingIndex)
return [firstPageIndex, DOTS, ...middleRange, DOTS, lastPageIndex]
}
}, [siblings, page, count]) as PaginationRange
const lastPage = paginationRange.at(-1)
const isFirstPage = page === 1
const isLastPage = page === lastPage
const handleNextPageClicked = useCallback(() => {
onChange(page + 1)
}, [page, onChange])
const handlePreviousPageClicked = useCallback(() => {
onChange(page - 1)
}, [page, onChange])
function handleMainPageClicked(pageNumber: number) {
return () => onChange(pageNumber)
}
return {
paginationRange,
isFirstPage,
isLastPage,
handleMainPageClicked,
handleNextPageClicked,
handlePreviousPageClicked,
}
}