From 33f3fc6137f94627a3087289e6a95c197ae34058 Mon Sep 17 00:00:00 2001 From: Andres Date: Sat, 7 Oct 2023 23:44:20 +0200 Subject: [PATCH] add cards table and pagination --- src/assets/icons/index.ts | 2 + src/assets/icons/keyboard-arrow-left.tsx | 27 +++ src/assets/icons/keyboard-arrow-right.tsx | 27 +++ src/components/decks/cards-table.tsx | 47 +++++ src/components/decks/decks-table.tsx | 62 ++++++ src/components/decks/index.ts | 2 + src/components/index.ts | 1 + src/components/ui/pagination/index.ts | 1 + .../ui/pagination/pagination.module.scss | 75 +++++++ src/components/ui/pagination/pagination.tsx | 192 ++++++++++++++++++ src/components/ui/pagination/usePagination.ts | 112 ++++++++++ src/pages/deck-page/deck-page.tsx | 32 +++ src/pages/decks-page/decks-page.tsx | 61 +----- src/router.tsx | 6 + src/services/decks/decks.service.ts | 10 +- src/services/decks/decks.types.ts | 21 ++ src/services/index.ts | 1 + src/styles/_tokens.scss | 1 + src/utils/date/format-date.ts | 5 + src/utils/date/index.ts | 1 + src/utils/index.ts | 1 + 21 files changed, 635 insertions(+), 52 deletions(-) create mode 100644 src/assets/icons/keyboard-arrow-left.tsx create mode 100644 src/assets/icons/keyboard-arrow-right.tsx create mode 100644 src/components/decks/cards-table.tsx create mode 100644 src/components/decks/index.ts create mode 100644 src/components/ui/pagination/index.ts create mode 100644 src/components/ui/pagination/pagination.module.scss create mode 100644 src/components/ui/pagination/pagination.tsx create mode 100644 src/components/ui/pagination/usePagination.ts create mode 100644 src/pages/deck-page/deck-page.tsx create mode 100644 src/utils/date/format-date.ts create mode 100644 src/utils/date/index.ts create mode 100644 src/utils/index.ts diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts index 1753e98..313263e 100644 --- a/src/assets/icons/index.ts +++ b/src/assets/icons/index.ts @@ -10,3 +10,5 @@ export { default as PersonOutline } from './person-outline' export { default as ChevronUp } from './chevron-up' export { default as Close } from './close' export { default as Search } from './search' +export { default as KeyboardArrowLeft } from './keyboard-arrow-left' +export { default as KeyboardArrowRight } from './keyboard-arrow-right' diff --git a/src/assets/icons/keyboard-arrow-left.tsx b/src/assets/icons/keyboard-arrow-left.tsx new file mode 100644 index 0000000..3fc43e6 --- /dev/null +++ b/src/assets/icons/keyboard-arrow-left.tsx @@ -0,0 +1,27 @@ +import { SVGProps, Ref, forwardRef, memo } from 'react' +const SvgComponent = (props: SVGProps, ref: Ref) => ( + + + + + + + + + + +) +const ForwardRef = forwardRef(SvgComponent) + +export default memo(ForwardRef) diff --git a/src/assets/icons/keyboard-arrow-right.tsx b/src/assets/icons/keyboard-arrow-right.tsx new file mode 100644 index 0000000..f693a42 --- /dev/null +++ b/src/assets/icons/keyboard-arrow-right.tsx @@ -0,0 +1,27 @@ +import { SVGProps, Ref, forwardRef, memo } from 'react' +const SvgComponent = (props: SVGProps, ref: Ref) => ( + + + + + + + + + + +) +const ForwardRef = forwardRef(SvgComponent) + +export default memo(ForwardRef) diff --git a/src/components/decks/cards-table.tsx b/src/components/decks/cards-table.tsx new file mode 100644 index 0000000..9d3b369 --- /dev/null +++ b/src/components/decks/cards-table.tsx @@ -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 ( + + + + {cards?.map(card => ( + + {card.question} + {card.answer} + {formatDate(card.updated)} + {card.grade} + + ))} + +
+ ) +} diff --git a/src/components/decks/decks-table.tsx b/src/components/decks/decks-table.tsx index e69de29..cc14eb6 100644 --- a/src/components/decks/decks-table.tsx +++ b/src/components/decks/decks-table.tsx @@ -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 ( + + + + {decks?.map(deck => ( + + + + {deck.name} + + + {deck.cardsCount} + {formatDate(deck.updated)} + {deck.author.name} + ... + + ))} + +
+ ) +} diff --git a/src/components/decks/index.ts b/src/components/decks/index.ts new file mode 100644 index 0000000..5246fa1 --- /dev/null +++ b/src/components/decks/index.ts @@ -0,0 +1,2 @@ +export * from './decks-table.tsx' +export * from './cards-table.tsx' diff --git a/src/components/index.ts b/src/components/index.ts index 8b676fe..bf720ab 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,3 +1,4 @@ export * from './ui' export * from './auth' export * from './profile' +export * from './decks' diff --git a/src/components/ui/pagination/index.ts b/src/components/ui/pagination/index.ts new file mode 100644 index 0000000..48e614c --- /dev/null +++ b/src/components/ui/pagination/index.ts @@ -0,0 +1 @@ +export * from './pagination' diff --git a/src/components/ui/pagination/pagination.module.scss b/src/components/ui/pagination/pagination.module.scss new file mode 100644 index 0000000..4b1faf9 --- /dev/null +++ b/src/components/ui/pagination/pagination.module.scss @@ -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; +} diff --git a/src/components/ui/pagination/pagination.tsx b/src/components/ui/pagination/pagination.tsx new file mode 100644 index 0000000..b72c8a5 --- /dev/null +++ b/src/components/ui/pagination/pagination.tsx @@ -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 = ({ + 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 ( +
+
+ + + + + +
+ + {showPerPageSelect && ( + + )} +
+ ) +} + +type NavigationButtonProps = { + onClick: () => void + disabled?: boolean +} + +type PageButtonProps = NavigationButtonProps & { + page: number + selected: boolean +} + +const Dots: FC = () => { + return +} +const PageButton: FC = ({ onClick, disabled, selected, page }) => { + return ( + + ) +} +const PrevButton: FC = ({ onClick, disabled }) => { + return ( + + ) +} + +const NextButton: FC = ({ onClick, disabled }) => { + return ( + + ) +} + +type MainPaginationButtonsProps = { + paginationRange: (number | string)[] + currentPage: number + onClick: (pageNumber: number) => () => void +} + +const MainPaginationButtons: FC = ({ + paginationRange, + currentPage, + onClick, +}) => { + return ( + <> + {paginationRange.map((page: number | string, index) => { + const isSelected = page === currentPage + + if (typeof page !== 'number') { + return + } + + return + })} + + ) +} + +export type PerPageSelectProps = { + perPage: number + perPageOptions: number[] + onPerPageChange: (itemPerPage: number) => void +} + +export const PerPageSelect: FC = ({ + perPage, + perPageOptions, + onPerPageChange, +}) => { + const selectOptions = perPageOptions.map(value => ({ + label: value, + value, + })) + + return ( +
+ Показать + {/**/} + на странице +
+ ) +} diff --git a/src/components/ui/pagination/usePagination.ts b/src/components/ui/pagination/usePagination.ts new file mode 100644 index 0000000..1300ca0 --- /dev/null +++ b/src/components/ui/pagination/usePagination.ts @@ -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, + } +} diff --git a/src/pages/deck-page/deck-page.tsx b/src/pages/deck-page/deck-page.tsx new file mode 100644 index 0000000..7554038 --- /dev/null +++ b/src/pages/deck-page/deck-page.tsx @@ -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 ( +
+ {deckData?.name} + + + + +
+ ) +} diff --git a/src/pages/decks-page/decks-page.tsx b/src/pages/decks-page/decks-page.tsx index 2191d7c..0297837 100644 --- a/src/pages/decks-page/decks-page.tsx +++ b/src/pages/decks-page/decks-page.tsx @@ -2,46 +2,15 @@ import { useState } from 'react' import s from './decks-page.module.scss' -import { - Button, - Page, - Typography, - Column, - Table, - TableBody, - TableCell, - TableHeader, - TableRow, - TextField, - Slider, -} from '@/components' +import { Button, Page, Slider, TextField, Typography } from '@/components' +import { DecksTable } from '@/components/decks/decks-table.tsx' +import { Pagination } from '@/components/ui/pagination' import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' 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 = () => { + const [currentPage, setCurrentPage] = useState(1) + const { data: decks } = useGetDecksQuery() const [activeTab, setActiveTab] = useState('my') const [range, setRange] = useState([0, 100]) @@ -67,20 +36,12 @@ export const DecksPage = () => { - - - - {decks?.items.map(deck => ( - - {deck.name} - {deck.cardsCount} - {deck.updated} - {deck.author.name} - ... - - ))} - -
+ + ) diff --git a/src/router.tsx b/src/router.tsx index e6feaa0..0a2fc4f 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -8,6 +8,8 @@ import { import { SignInPage, DecksPage } from './pages' +import { DeckPage } from '@/pages/deck-page/deck-page.tsx' + const publicRoutes: RouteObject[] = [ { element: , @@ -25,6 +27,10 @@ const privateRoutes: RouteObject[] = [ path: '/', element: , }, + { + path: '/decks/:deckId', + element: , + }, ] const router = createBrowserRouter([ diff --git a/src/services/decks/decks.service.ts b/src/services/decks/decks.service.ts index fb995e1..39c3d53 100644 --- a/src/services/decks/decks.service.ts +++ b/src/services/decks/decks.service.ts @@ -1,4 +1,4 @@ -import { DecksResponse } from './decks.types' +import { CardsResponse, DeckResponse, DecksResponse } from './decks.types' import { baseApi } from '@/services' @@ -7,7 +7,13 @@ const decksService = baseApi.injectEndpoints({ getDecks: builder.query({ query: () => `v1/decks`, }), + getDeckById: builder.query({ + query: ({ id }) => `v1/decks/${id}`, + }), + getDeckCards: builder.query({ + query: ({ id }) => `v1/decks/${id}/cards`, + }), }), }) -export const { useGetDecksQuery } = decksService +export const { useGetDecksQuery, useGetDeckByIdQuery, useGetDeckCardsQuery } = decksService diff --git a/src/services/decks/decks.types.ts b/src/services/decks/decks.types.ts index 742918f..7ee9a3c 100644 --- a/src/services/decks/decks.types.ts +++ b/src/services/decks/decks.types.ts @@ -31,3 +31,24 @@ export type DecksResponse = { pagination: Pagination 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 +} diff --git a/src/services/index.ts b/src/services/index.ts index 02efb7a..a857025 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1 +1,2 @@ export * from './base-api' +export * from './decks' diff --git a/src/styles/_tokens.scss b/src/styles/_tokens.scss index 0fdbcaa..93e75d9 100644 --- a/src/styles/_tokens.scss +++ b/src/styles/_tokens.scss @@ -1,5 +1,6 @@ :root { /* outlines */ + --color-outline-focus: var(--color-info-900); --outline-focus: 2px solid var(--color-outline-focus); /* transitions */ diff --git a/src/utils/date/format-date.ts b/src/utils/date/format-date.ts new file mode 100644 index 0000000..15e9ed1 --- /dev/null +++ b/src/utils/date/format-date.ts @@ -0,0 +1,5 @@ +export function formatDate(date: string | undefined) { + if (!date) return '' + + return new Date(date).toLocaleDateString('ru-RU') +} diff --git a/src/utils/date/index.ts b/src/utils/date/index.ts new file mode 100644 index 0000000..8d1fd92 --- /dev/null +++ b/src/utils/date/index.ts @@ -0,0 +1 @@ +export { formatDate } from './format-date' diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..edf1e3c --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1 @@ +export * from './date'