mirror of
https://github.com/ershisan99/flashcards-example-project.git
synced 2025-12-16 20:59:27 +00:00
add cards table and pagination
This commit is contained in:
@@ -10,3 +10,5 @@ export { default as PersonOutline } from './person-outline'
|
|||||||
export { default as ChevronUp } from './chevron-up'
|
export { default as ChevronUp } from './chevron-up'
|
||||||
export { default as Close } from './close'
|
export { default as Close } from './close'
|
||||||
export { default as Search } from './search'
|
export { default as Search } from './search'
|
||||||
|
export { default as KeyboardArrowLeft } from './keyboard-arrow-left'
|
||||||
|
export { default as KeyboardArrowRight } from './keyboard-arrow-right'
|
||||||
|
|||||||
27
src/assets/icons/keyboard-arrow-left.tsx
Normal file
27
src/assets/icons/keyboard-arrow-left.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { SVGProps, Ref, forwardRef, memo } from 'react'
|
||||||
|
const SvgComponent = (props: SVGProps<SVGSVGElement>, ref: Ref<SVGSVGElement>) => (
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<g clipPath="url(#clip0_5928_3055)">
|
||||||
|
<path
|
||||||
|
d="M10.2733 11.06L7.21998 8L10.2733 4.94L9.33331 4L5.33331 8L9.33331 12L10.2733 11.06Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_5928_3055">
|
||||||
|
<rect width="16" height="16" fill="white" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
const ForwardRef = forwardRef(SvgComponent)
|
||||||
|
|
||||||
|
export default memo(ForwardRef)
|
||||||
27
src/assets/icons/keyboard-arrow-right.tsx
Normal file
27
src/assets/icons/keyboard-arrow-right.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { SVGProps, Ref, forwardRef, memo } from 'react'
|
||||||
|
const SvgComponent = (props: SVGProps<SVGSVGElement>, ref: Ref<SVGSVGElement>) => (
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<g clipPath="url(#clip0_5928_3027)">
|
||||||
|
<path
|
||||||
|
d="M5.72665 11.06L8.77999 8L5.72665 4.94L6.66665 4L10.6667 8L6.66665 12L5.72665 11.06Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_5928_3027">
|
||||||
|
<rect width="16" height="16" fill="white" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
const ForwardRef = forwardRef(SvgComponent)
|
||||||
|
|
||||||
|
export default memo(ForwardRef)
|
||||||
47
src/components/decks/cards-table.tsx
Normal file
47
src/components/decks/cards-table.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
2
src/components/decks/index.ts
Normal file
2
src/components/decks/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './decks-table.tsx'
|
||||||
|
export * from './cards-table.tsx'
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './ui'
|
export * from './ui'
|
||||||
export * from './auth'
|
export * from './auth'
|
||||||
export * from './profile'
|
export * from './profile'
|
||||||
|
export * from './decks'
|
||||||
|
|||||||
1
src/components/ui/pagination/index.ts
Normal file
1
src/components/ui/pagination/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './pagination'
|
||||||
75
src/components/ui/pagination/pagination.module.scss
Normal file
75
src/components/ui/pagination/pagination.module.scss
Normal 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;
|
||||||
|
}
|
||||||
192
src/components/ui/pagination/pagination.tsx
Normal file
192
src/components/ui/pagination/pagination.tsx
Normal 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}>…</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
112
src/components/ui/pagination/usePagination.ts
Normal file
112
src/components/ui/pagination/usePagination.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/pages/deck-page/deck-page.tsx
Normal file
32
src/pages/deck-page/deck-page.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import { Link, useParams } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { Button, TextField, Typography, CardsTable } from '@/components'
|
||||||
|
import { Pagination } from '@/components/ui/pagination'
|
||||||
|
import { useGetDeckByIdQuery, useGetDeckCardsQuery } from '@/services'
|
||||||
|
|
||||||
|
export const DeckPage = () => {
|
||||||
|
const { deckId } = useParams()
|
||||||
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
|
const { data: deckData } = useGetDeckByIdQuery({ id: deckId || '' })
|
||||||
|
const { data: cardsData } = useGetDeckCardsQuery({ id: deckId || '' })
|
||||||
|
|
||||||
|
const learnLink = `/decks/${deckId}/learn`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Typography variant={'large'}>{deckData?.name}</Typography>
|
||||||
|
<Button as={Link} to={learnLink}>
|
||||||
|
Learn
|
||||||
|
</Button>
|
||||||
|
<TextField search placeholder={'Search cards'} />
|
||||||
|
<CardsTable cards={cardsData?.items} />
|
||||||
|
<Pagination
|
||||||
|
count={cardsData?.pagination?.totalPages || 1}
|
||||||
|
page={currentPage}
|
||||||
|
onChange={setCurrentPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,46 +2,15 @@ import { useState } from 'react'
|
|||||||
|
|
||||||
import s from './decks-page.module.scss'
|
import s from './decks-page.module.scss'
|
||||||
|
|
||||||
import {
|
import { Button, Page, Slider, TextField, Typography } from '@/components'
|
||||||
Button,
|
import { DecksTable } from '@/components/decks/decks-table.tsx'
|
||||||
Page,
|
import { Pagination } from '@/components/ui/pagination'
|
||||||
Typography,
|
|
||||||
Column,
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
TextField,
|
|
||||||
Slider,
|
|
||||||
} from '@/components'
|
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
import { useGetDecksQuery } from '@/services/decks'
|
import { useGetDecksQuery } from '@/services/decks'
|
||||||
|
|
||||||
const columns: Column[] = [
|
|
||||||
{
|
|
||||||
key: 'name',
|
|
||||||
title: 'Name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'cardsCount',
|
|
||||||
title: 'Cards',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'updated',
|
|
||||||
title: 'Last Updated',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'author',
|
|
||||||
title: 'Created By',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'actions',
|
|
||||||
title: '',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export const DecksPage = () => {
|
export const DecksPage = () => {
|
||||||
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
|
|
||||||
const { data: decks } = useGetDecksQuery()
|
const { data: decks } = useGetDecksQuery()
|
||||||
const [activeTab, setActiveTab] = useState('my')
|
const [activeTab, setActiveTab] = useState('my')
|
||||||
const [range, setRange] = useState([0, 100])
|
const [range, setRange] = useState([0, 100])
|
||||||
@@ -67,20 +36,12 @@ export const DecksPage = () => {
|
|||||||
<Slider onValueCommit={setRange} value={rangeValue} onValueChange={setRangeValue} />
|
<Slider onValueCommit={setRange} value={rangeValue} onValueChange={setRangeValue} />
|
||||||
<Button variant={'secondary'}>Clear filters</Button>
|
<Button variant={'secondary'}>Clear filters</Button>
|
||||||
</div>
|
</div>
|
||||||
<Table>
|
<DecksTable decks={decks?.items} />
|
||||||
<TableHeader columns={columns} />
|
<Pagination
|
||||||
<TableBody>
|
count={decks?.pagination?.totalPages || 1}
|
||||||
{decks?.items.map(deck => (
|
page={currentPage}
|
||||||
<TableRow key={deck.id}>
|
onChange={setCurrentPage}
|
||||||
<TableCell>{deck.name}</TableCell>
|
/>
|
||||||
<TableCell>{deck.cardsCount}</TableCell>
|
|
||||||
<TableCell>{deck.updated}</TableCell>
|
|
||||||
<TableCell>{deck.author.name}</TableCell>
|
|
||||||
<TableCell>...</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
|
|
||||||
import { SignInPage, DecksPage } from './pages'
|
import { SignInPage, DecksPage } from './pages'
|
||||||
|
|
||||||
|
import { DeckPage } from '@/pages/deck-page/deck-page.tsx'
|
||||||
|
|
||||||
const publicRoutes: RouteObject[] = [
|
const publicRoutes: RouteObject[] = [
|
||||||
{
|
{
|
||||||
element: <Outlet />,
|
element: <Outlet />,
|
||||||
@@ -25,6 +27,10 @@ const privateRoutes: RouteObject[] = [
|
|||||||
path: '/',
|
path: '/',
|
||||||
element: <DecksPage />,
|
element: <DecksPage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/decks/:deckId',
|
||||||
|
element: <DeckPage />,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DecksResponse } from './decks.types'
|
import { CardsResponse, DeckResponse, DecksResponse } from './decks.types'
|
||||||
|
|
||||||
import { baseApi } from '@/services'
|
import { baseApi } from '@/services'
|
||||||
|
|
||||||
@@ -7,7 +7,13 @@ const decksService = baseApi.injectEndpoints({
|
|||||||
getDecks: builder.query<DecksResponse, void>({
|
getDecks: builder.query<DecksResponse, void>({
|
||||||
query: () => `v1/decks`,
|
query: () => `v1/decks`,
|
||||||
}),
|
}),
|
||||||
|
getDeckById: builder.query<DeckResponse, { id: string }>({
|
||||||
|
query: ({ id }) => `v1/decks/${id}`,
|
||||||
|
}),
|
||||||
|
getDeckCards: builder.query<CardsResponse, { id: string }>({
|
||||||
|
query: ({ id }) => `v1/decks/${id}/cards`,
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const { useGetDecksQuery } = decksService
|
export const { useGetDecksQuery, useGetDeckByIdQuery, useGetDeckCardsQuery } = decksService
|
||||||
|
|||||||
@@ -31,3 +31,24 @@ export type DecksResponse = {
|
|||||||
pagination: Pagination
|
pagination: Pagination
|
||||||
items: Deck[]
|
items: Deck[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DeckResponse = Deck
|
||||||
|
|
||||||
|
export type CardsResponse = {
|
||||||
|
pagination: Pagination
|
||||||
|
items: Card[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Card = {
|
||||||
|
id: string
|
||||||
|
question: string
|
||||||
|
answer: string
|
||||||
|
deckId: string
|
||||||
|
questionImg?: string | null
|
||||||
|
answerImg?: string | null
|
||||||
|
created: string
|
||||||
|
updated: string
|
||||||
|
shots: number
|
||||||
|
grade: number
|
||||||
|
userId: string
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export * from './base-api'
|
export * from './base-api'
|
||||||
|
export * from './decks'
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
:root {
|
:root {
|
||||||
/* outlines */
|
/* outlines */
|
||||||
|
--color-outline-focus: var(--color-info-900);
|
||||||
--outline-focus: 2px solid var(--color-outline-focus);
|
--outline-focus: 2px solid var(--color-outline-focus);
|
||||||
|
|
||||||
/* transitions */
|
/* transitions */
|
||||||
|
|||||||
5
src/utils/date/format-date.ts
Normal file
5
src/utils/date/format-date.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export function formatDate(date: string | undefined) {
|
||||||
|
if (!date) return ''
|
||||||
|
|
||||||
|
return new Date(date).toLocaleDateString('ru-RU')
|
||||||
|
}
|
||||||
1
src/utils/date/index.ts
Normal file
1
src/utils/date/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { formatDate } from './format-date'
|
||||||
1
src/utils/index.ts
Normal file
1
src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './date'
|
||||||
Reference in New Issue
Block a user