diff --git a/package.json b/package.json index 6aa27f2..3dda9e3 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,13 @@ "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-radio-group": "^1.1.3", + "@reduxjs/toolkit": "^2.0.1", "@storybook/theming": "^7.2.1", "clsx": "^2.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.45.2", + "react-redux": "^9.0.4", "react-router-dom": "^6.14.2", "react-toastify": "^9.1.3", "remeda": "^1.24.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e03f24..db32e5c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ dependencies: '@radix-ui/react-radio-group': specifier: ^1.1.3 version: 1.1.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0) + '@reduxjs/toolkit': + specifier: ^2.0.1 + version: 2.0.1(react-redux@9.0.4)(react@18.2.0) '@storybook/theming': specifier: ^7.2.1 version: 7.2.1(react-dom@18.2.0)(react@18.2.0) @@ -35,6 +38,9 @@ dependencies: react-hook-form: specifier: ^7.45.2 version: 7.45.2(react@18.2.0) + react-redux: + specifier: ^9.0.4 + version: 9.0.4(@types/react@18.2.15)(react@18.2.0)(redux@5.0.0) react-router-dom: specifier: ^6.14.2 version: 6.14.2(react-dom@18.2.0)(react@18.2.0) @@ -2672,6 +2678,25 @@ packages: dependencies: '@babel/runtime': 7.22.6 + /@reduxjs/toolkit@2.0.1(react-redux@9.0.4)(react@18.2.0): + resolution: {integrity: sha512-fxIjrR9934cmS8YXIGd9e7s1XRsEU++aFc9DVNMFMRTM5Vtsg2DCRMj21eslGtDt43IUf9bJL3h5bwUlZleibA==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + dependencies: + immer: 10.0.3 + react: 18.2.0 + react-redux: 9.0.4(@types/react@18.2.15)(react@18.2.0)(redux@5.0.0) + redux: 5.0.0 + redux-thunk: 3.1.0(redux@5.0.0) + reselect: 5.0.1 + dev: false + /@remix-run/router@1.7.2: resolution: {integrity: sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==} engines: {node: '>=14'} @@ -4073,6 +4098,10 @@ packages: resolution: {integrity: sha512-cputDpIbFgLUaGQn6Vqg3/YsJwxUwHLO13v3i5ouxT4lat0khip9AEWxtERujXV9wxIB1EyF97BSJFt6vpdI8g==} dev: true + /@types/use-sync-external-store@0.0.3: + resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} + dev: false + /@types/yargs-parser@21.0.0: resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} dev: true @@ -6726,6 +6755,10 @@ packages: engines: {node: '>= 4'} dev: true + /immer@10.0.3: + resolution: {integrity: sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==} + dev: false + /immutable@4.3.1: resolution: {integrity: sha512-lj9cnmB/kVS0QHsJnYKD1uo3o39nrbKxszjnqS9Fr6NB7bZzW45U6WSGBPKXDL/CvDKqDNPA4r3DoDQ8GTxo2A==} dev: true @@ -8368,6 +8401,28 @@ packages: resolution: {integrity: sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==} dev: true + /react-redux@9.0.4(@types/react@18.2.15)(react@18.2.0)(redux@5.0.0): + resolution: {integrity: sha512-9J1xh8sWO0vYq2sCxK2My/QO7MzUMRi3rpiILP/+tDr8krBHixC6JMM17fMK88+Oh3e4Ae6/sHIhNBgkUivwFA==} + peerDependencies: + '@types/react': ^18.2.25 + react: ^18.0 + react-native: '>=0.69' + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-native: + optional: true + redux: + optional: true + dependencies: + '@types/react': 18.2.15 + '@types/use-sync-external-store': 0.0.3 + react: 18.2.0 + redux: 5.0.0 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false + /react-refresh@0.14.0: resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} engines: {node: '>=0.10.0'} @@ -8565,6 +8620,18 @@ packages: strip-indent: 4.0.0 dev: true + /redux-thunk@3.1.0(redux@5.0.0): + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + dependencies: + redux: 5.0.0 + dev: false + + /redux@5.0.0: + resolution: {integrity: sha512-blLIYmYetpZMET6Q6uCY7Jtl/Im5OBldy+vNPauA8vvsdqyt66oep4EUpAMWNHauTC6xa9JuRPhRB72rY82QGA==} + dev: false + /regenerate-unicode-properties@10.1.0: resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==} engines: {node: '>=4'} @@ -8649,6 +8716,10 @@ packages: engines: {node: '>=0.10.5'} dev: true + /reselect@5.0.1: + resolution: {integrity: sha512-D72j2ubjgHpvuCiORWkOUxndHJrxDaSolheiz5CO+roz8ka97/4msh2E8F5qay4GawR5vzBt5MkbDHT+Rdy/Wg==} + dev: false + /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -9814,6 +9885,14 @@ packages: react: 18.2.0 tslib: 2.6.1 + /use-sync-external-store@1.2.0(react@18.2.0): + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} diff --git a/src/App.tsx b/src/App.tsx index 5541660..71e489e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,3 +1,15 @@ +import { Provider } from 'react-redux' + +import { Layout } from '@/components/ui/layout/layout.tsx' +import { Router } from '@/router.tsx' +import { store } from '@/services/store' + export function App() { - return
Hello
+ return ( + + + + + + ) } diff --git a/src/assets/icons/camera.tsx b/src/assets/icons/camera.tsx index cfb48ff..08ff491 100644 --- a/src/assets/icons/camera.tsx +++ b/src/assets/icons/camera.tsx @@ -1,63 +1,71 @@ -import { SVGProps, Ref, forwardRef, memo } from 'react' +import { Ref, SVGProps, forwardRef, memo } from 'react' const SvgComponent = (props: SVGProps, ref: Ref) => ( - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + ) const ForwardRef = forwardRef(SvgComponent) diff --git a/src/assets/icons/check.tsx b/src/assets/icons/check.tsx index 1f35bda..9e4ab19 100644 --- a/src/assets/icons/check.tsx +++ b/src/assets/icons/check.tsx @@ -1,19 +1,21 @@ -import { SVGProps, Ref, forwardRef, memo } from 'react' +import { Ref, SVGProps, forwardRef, memo } from 'react' const SvgComponent = (props: SVGProps, ref: Ref) => ( - - - + + + ) const ForwardRef = forwardRef(SvgComponent) diff --git a/src/assets/icons/chevron-up.tsx b/src/assets/icons/chevron-up.tsx index 15c33e1..e841c88 100644 --- a/src/assets/icons/chevron-up.tsx +++ b/src/assets/icons/chevron-up.tsx @@ -1,26 +1,28 @@ -import { SVGProps, Ref, forwardRef, memo } from 'react' +import { Ref, SVGProps, forwardRef, memo } from 'react' const ChevronUp = (props: SVGProps, ref: Ref) => ( - - - - - - - - - - + + + + + + + + + + ) const ForwardRef = forwardRef(ChevronUp) const Memo = memo(ForwardRef) diff --git a/src/assets/icons/close.tsx b/src/assets/icons/close.tsx index 3b8a146..ec16872 100644 --- a/src/assets/icons/close.tsx +++ b/src/assets/icons/close.tsx @@ -1,29 +1,37 @@ -import { SVGProps, Ref, forwardRef, memo } from 'react' +import { Ref, SVGProps, forwardRef, memo } from 'react' const SvgComponent = (props: SVGProps, ref: Ref) => ( - - - - - - - - - - + + + + + + + + + + + - ) const ForwardRef = forwardRef(SvgComponent) diff --git a/src/assets/icons/edit-2-outline.tsx b/src/assets/icons/edit-2-outline.tsx new file mode 100644 index 0000000..5a38c55 --- /dev/null +++ b/src/assets/icons/edit-2-outline.tsx @@ -0,0 +1,33 @@ +import { SVGProps } from 'react' +const SvgComponent = (props: SVGProps) => ( + + + + + + + + + + + +) + +export default SvgComponent diff --git a/src/assets/icons/edit.tsx b/src/assets/icons/edit.tsx index 586cc58..d0ca114 100644 --- a/src/assets/icons/edit.tsx +++ b/src/assets/icons/edit.tsx @@ -1,30 +1,34 @@ -import { SVGProps, Ref, forwardRef, memo } from 'react' +import { Ref, SVGProps, forwardRef, memo } from 'react' const SvgComponent = (props: SVGProps, ref: Ref) => ( - - - - - - - - - - - + + + + + + + + + + + ) const ForwardRef = forwardRef(SvgComponent) const Memo = memo(ForwardRef) diff --git a/src/assets/icons/email.tsx b/src/assets/icons/email.tsx index f424822..a7dda26 100644 --- a/src/assets/icons/email.tsx +++ b/src/assets/icons/email.tsx @@ -1,73 +1,100 @@ -import { SVGProps, Ref, forwardRef, memo } from 'react' +import { Ref, SVGProps, forwardRef, memo } from 'react' const SvgComponent = (props: SVGProps, ref: Ref) => ( - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + ) const ForwardRef = forwardRef(SvgComponent) diff --git a/src/assets/icons/eye.tsx b/src/assets/icons/eye.tsx index 12e4b70..e029337 100644 --- a/src/assets/icons/eye.tsx +++ b/src/assets/icons/eye.tsx @@ -1,21 +1,23 @@ -import { SVGProps, Ref, forwardRef, memo } from 'react' +import { Ref, SVGProps, forwardRef, memo } from 'react' const SvgComponent = (props: SVGProps, ref: Ref) => ( - - - + + + ) const ForwardRef = forwardRef(SvgComponent) diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts index adf3ef0..efc4a51 100644 --- a/src/assets/icons/index.ts +++ b/src/assets/icons/index.ts @@ -1,11 +1,17 @@ -export { default as Eye } from './eye' -export { default as VisibilityOff } from './visibility-off' -export { default as Check } from './check' -export { default as Email } from './email' export { default as Camera } from './camera' -export { default as Logout } from './logout' -export { default as Edit } from './edit' -export { default as Logo } from './logo' -export { default as PersonOutline } from './person-outline' +export { default as Check } from './check' export { default as ChevronUp } from './chevron-up' export { default as Close } from './close' +export { default as Edit } from './edit' +export { default as Edit2Outline } from './edit-2-outline' +export { default as Email } from './email' +export { default as Eye } from './eye' +export { default as KeyboardArrowLeft } from './keyboard-arrow-left' +export { default as KeyboardArrowRight } from './keyboard-arrow-right' +export { default as Logo } from './logo' +export { default as Logout } from './logout' +export { default as PersonOutline } from './person-outline' +export { default as PlayCircleOutline } from './play-circle-outline' +export { default as Search } from './search' +export { default as TrashOutline } from './trash-outline' +export { default as VisibilityOff } from './visibility-off' diff --git a/src/assets/icons/keyboard-arrow-left.tsx b/src/assets/icons/keyboard-arrow-left.tsx new file mode 100644 index 0000000..424847d --- /dev/null +++ b/src/assets/icons/keyboard-arrow-left.tsx @@ -0,0 +1,29 @@ +import { Ref, SVGProps, 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..2ec3a5e --- /dev/null +++ b/src/assets/icons/keyboard-arrow-right.tsx @@ -0,0 +1,29 @@ +import { Ref, SVGProps, forwardRef, memo } from 'react' +const SvgComponent = (props: SVGProps, ref: Ref) => ( + + + + + + + + + + +) +const ForwardRef = forwardRef(SvgComponent) + +export default memo(ForwardRef) diff --git a/src/assets/icons/logo.tsx b/src/assets/icons/logo.tsx index 0df7cbd..3ecb322 100644 --- a/src/assets/icons/logo.tsx +++ b/src/assets/icons/logo.tsx @@ -1,30 +1,34 @@ import { SVGProps } from 'react' const SvgComponent = (props: SVGProps) => ( - - - - - + + + + + ) export default SvgComponent diff --git a/src/assets/icons/logout.tsx b/src/assets/icons/logout.tsx index a7c19af..8d10e81 100644 --- a/src/assets/icons/logout.tsx +++ b/src/assets/icons/logout.tsx @@ -1,31 +1,21 @@ -import { SVGProps, Ref, forwardRef, memo } from 'react' -const SvgComponent = (props: SVGProps, ref: Ref) => ( - - - - - - - - - - - +import { SVGProps, Ref, forwardRef, memo } from "react" +const SvgComponent = ( + props: SVGProps, + ref: Ref +) => ( + + + + + ) const ForwardRef = forwardRef(SvgComponent) - -export default memo(ForwardRef) +const Memo = memo(ForwardRef) +export default Memo diff --git a/src/assets/icons/person-outline.tsx b/src/assets/icons/person-outline.tsx index dd172d1..f041d38 100644 --- a/src/assets/icons/person-outline.tsx +++ b/src/assets/icons/person-outline.tsx @@ -1,22 +1,26 @@ import { SVGProps } from 'react' const SvgComponent = (props: SVGProps) => ( - - - - - - - - - - + + + + + + + + + + ) export default SvgComponent diff --git a/src/assets/icons/play-circle-outline.tsx b/src/assets/icons/play-circle-outline.tsx new file mode 100644 index 0000000..34634e2 --- /dev/null +++ b/src/assets/icons/play-circle-outline.tsx @@ -0,0 +1,33 @@ +import { SVGProps } from 'react' +const SvgComponent = (props: SVGProps) => ( + + + + + + + + + + + +) + +export default SvgComponent diff --git a/src/assets/icons/search.tsx b/src/assets/icons/search.tsx new file mode 100644 index 0000000..55c329d --- /dev/null +++ b/src/assets/icons/search.tsx @@ -0,0 +1,28 @@ +import { Ref, SVGProps, forwardRef, memo } from 'react' +const SvgComponent = (props: SVGProps, ref: Ref) => ( + + + + + + + + + + +) + +export default memo(forwardRef(SvgComponent)) diff --git a/src/assets/icons/trash-outline.tsx b/src/assets/icons/trash-outline.tsx new file mode 100644 index 0000000..2a5d157 --- /dev/null +++ b/src/assets/icons/trash-outline.tsx @@ -0,0 +1,27 @@ +import { SVGProps } from 'react' +const SvgComponent = (props: SVGProps) => ( + + + + + + + + + + +) + +export default SvgComponent diff --git a/src/assets/icons/visibility-off.tsx b/src/assets/icons/visibility-off.tsx index c45f878..9cdf760 100644 --- a/src/assets/icons/visibility-off.tsx +++ b/src/assets/icons/visibility-off.tsx @@ -1,19 +1,21 @@ -import { SVGProps, Ref, forwardRef, memo } from 'react' +import { Ref, SVGProps, forwardRef, memo } from 'react' const SvgComponent = (props: SVGProps, ref: Ref) => ( - - - + + + ) const ForwardRef = forwardRef(SvgComponent) diff --git a/src/components/ui/header/header.module.scss b/src/components/ui/header/header.module.scss new file mode 100644 index 0000000..8d21589 --- /dev/null +++ b/src/components/ui/header/header.module.scss @@ -0,0 +1,13 @@ +.header { + width: 100%; + position: fixed; + left: 0; + top: 0; + right: 0; + height: var(--header-height); + padding: 12px 24px; + display: flex; + align-items: center; + border-bottom: 1px solid var(--Dark-500, #333); + background: var(--Dark-700, #171717); +} diff --git a/src/components/ui/header/header.stories.tsx b/src/components/ui/header/header.stories.tsx new file mode 100644 index 0000000..4c1eeaf --- /dev/null +++ b/src/components/ui/header/header.stories.tsx @@ -0,0 +1,33 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import { Header } from './index.ts' + +import { BrowserRouterDecorator } from '@/app/providers' + +const meta = { + title: 'Components/Header', + component: Header, + tags: ['autodocs'], + decorators: [BrowserRouterDecorator], +} satisfies Meta + +export default meta +type Story = StoryObj + +export const AuthorizedUser: Story = { + args: { + data: { + name: 'User name', + email: 'user-email.gmail.com', + avatar: '', + }, + logout: () => {}, + }, +} + +export const UnauthorizedUser: Story = { + args: { + data: null, + logout: () => {}, + }, +} diff --git a/src/components/ui/header/header.tsx b/src/components/ui/header/header.tsx new file mode 100644 index 0000000..b2e3389 --- /dev/null +++ b/src/components/ui/header/header.tsx @@ -0,0 +1,4 @@ +import s from './header.module.scss' +export const Header = () => { + return
Hello
+} diff --git a/src/components/ui/header/index.ts b/src/components/ui/header/index.ts new file mode 100644 index 0000000..50bff68 --- /dev/null +++ b/src/components/ui/header/index.ts @@ -0,0 +1 @@ +export { Header } from './header.tsx' diff --git a/src/components/ui/layout/layout.module.scss b/src/components/ui/layout/layout.module.scss new file mode 100644 index 0000000..27bf29d --- /dev/null +++ b/src/components/ui/layout/layout.module.scss @@ -0,0 +1,10 @@ +.layout { +} +.main { + padding-top: var(--header-height); + padding-inline: 24px; + display: flex; + flex-direction: column; + align-items: center; + width: 100%; +} diff --git a/src/components/ui/layout/layout.tsx b/src/components/ui/layout/layout.tsx new file mode 100644 index 0000000..6fa0565 --- /dev/null +++ b/src/components/ui/layout/layout.tsx @@ -0,0 +1,21 @@ +import { ComponentPropsWithoutRef, CSSProperties, ElementRef, forwardRef } from 'react' + +import { clsx } from 'clsx' + +import s from './layout.module.scss' + +import { Header } from '@/components/ui/header' + +type Props = ComponentPropsWithoutRef<'div'> & { + contentMarginTop?: CSSProperties['marginTop'] +} +export const Layout = forwardRef, Props>( + ({ className, children, ...rest }, ref) => { + return ( +
+
+
{children}
+
+ ) + } +) diff --git a/src/components/ui/page/page.module.scss b/src/components/ui/page/page.module.scss new file mode 100644 index 0000000..aadf610 --- /dev/null +++ b/src/components/ui/page/page.module.scss @@ -0,0 +1,6 @@ +.container { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; +} diff --git a/src/components/ui/page/page.tsx b/src/components/ui/page/page.tsx new file mode 100644 index 0000000..81eff3b --- /dev/null +++ b/src/components/ui/page/page.tsx @@ -0,0 +1,14 @@ +import { ComponentPropsWithoutRef, CSSProperties } from 'react' + +import { clsx } from 'clsx' + +import s from './page.module.scss' +type Props = ComponentPropsWithoutRef<'div'> & { + mt?: CSSProperties['marginTop'] +} +export const Page = ({ className, style, mt = '33px', ...rest }: Props) => { + const classes = clsx(className, s.container) + const styles: CSSProperties = { marginTop: mt, ...style } + + return
+} 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..43018ac --- /dev/null +++ b/src/components/ui/pagination/pagination.tsx @@ -0,0 +1,194 @@ +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 = + | { + onPerPageChange: (itemPerPage: number) => void + perPage: number + perPageOptions: number[] + } + | { + onPerPageChange?: never + perPage?: null + perPageOptions?: never + } + +export type PaginationProps = { + count: number + onChange: (page: number) => void + onPerPageChange?: (itemPerPage: number) => void + page: number + perPage?: number + perPageOptions?: number[] + siblings?: number +} & PaginationConditionals + +const classNames = { + container: s.container, + dots: s.dots, + icon: s.icon, + item: s.item, + pageButton(selected?: boolean) { + return clsx(this.item, selected && s.selected) + }, + root: s.root, + select: s.select, + selectBox: s.selectBox, +} + +export const Pagination: FC = ({ + count, + onChange, + onPerPageChange, + page, + perPage = null, + perPageOptions, + siblings, +}) => { + const { + handleMainPageClicked, + handleNextPageClicked, + handlePreviousPageClicked, + isFirstPage, + isLastPage, + paginationRange, + } = usePagination({ + count, + onChange, + page, + siblings, + }) + + const showPerPageSelect = !!perPage && !!perPageOptions && !!onPerPageChange + + return ( +
+
+ + + + + +
+ + {showPerPageSelect && ( + + )} +
+ ) +} + +type NavigationButtonProps = { + disabled?: boolean + onClick: () => void +} + +type PageButtonProps = NavigationButtonProps & { + page: number + selected: boolean +} + +const Dots: FC = () => { + return +} +const PageButton: FC = ({ disabled, onClick, page, selected }) => { + return ( + + ) +} +const PrevButton: FC = ({ disabled, onClick }) => { + return ( + + ) +} + +const NextButton: FC = ({ disabled, onClick }) => { + return ( + + ) +} + +type MainPaginationButtonsProps = { + currentPage: number + onClick: (pageNumber: number) => () => void + paginationRange: (number | string)[] +} + +const MainPaginationButtons: FC = ({ + currentPage, + onClick, + paginationRange, +}) => { + return ( + <> + {paginationRange.map((page: number | string, index) => { + const isSelected = page === currentPage + + if (typeof page !== 'number') { + return + } + + return + })} + + ) +} + +export type PerPageSelectProps = { + onPerPageChange: (itemPerPage: number) => void + perPage: number + perPageOptions: number[] +} + +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..be7d78e --- /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) => { + const 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 + onChange: (pageNumber: number) => void + page: number + siblings?: number +} + +type PaginationRange = ('...' | number)[] + +export const usePagination = ({ count, onChange, page, siblings = 1 }: 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) { + const leftItemCount = 3 + 2 * siblings + const leftRange = range(1, leftItemCount) + + return [...leftRange, DOTS, count] + } + + /* + Case 3: No right dots to show, but left dots to be shown + */ + if (shouldShowLeftDots && !shouldShowRightDots) { + const rightItemCount = 3 + 2 * siblings + const rightRange = range(count - rightItemCount + 1, count) + + return [firstPageIndex, DOTS, ...rightRange] + } + + /* + Case 4: Both left and right dots to be shown + */ + if (shouldShowLeftDots && shouldShowRightDots) { + const 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 { + handleMainPageClicked, + handleNextPageClicked, + handlePreviousPageClicked, + isFirstPage, + isLastPage, + paginationRange, + } +} diff --git a/src/pages/auth/login/login.tsx b/src/pages/auth/login/login.tsx new file mode 100644 index 0000000..0de8e1f --- /dev/null +++ b/src/pages/auth/login/login.tsx @@ -0,0 +1,10 @@ +import { SignIn } from '@/components' +import { Page } from '@/components/ui/page/page.tsx' + +export const LoginPage = () => { + return ( + + {}} /> + + ) +} diff --git a/src/pages/decks/decks.module.scss b/src/pages/decks/decks.module.scss new file mode 100644 index 0000000..ed4a4e1 --- /dev/null +++ b/src/pages/decks/decks.module.scss @@ -0,0 +1,25 @@ +.header { + display: flex; + align-items: center; + width: 100%; + justify-content: space-between; +} + +.filter { + display: grid; + grid-template-columns: repeat(4, 1fr); + margin-block: 36px; + align-items: center; + width: 100%; + gap: 24px; + + > * { + flex-grow: 1; + flex-shrink: 1; + } +} + +.tabs { + display: flex; + align-items: center; +} diff --git a/src/pages/decks/decks.tsx b/src/pages/decks/decks.tsx new file mode 100644 index 0000000..ea4bd4b --- /dev/null +++ b/src/pages/decks/decks.tsx @@ -0,0 +1,101 @@ +import { useSearchParams } from 'react-router-dom' + +import s from './decks.module.scss' + +import { Button, TextField, Typography } from '@/components' +import { Page } from '@/components/ui/page/page.tsx' +import { Pagination } from '@/components/ui/pagination' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeadCell, + TableRow, +} from '@/components/ui/table' +import { useCreateDeckMutation, useGetDecksQuery } from '@/services/decks/decks.service.ts' + +export const Decks = () => { + const [searchParams, setSearchParams] = useSearchParams({ page: '1', name: '' }) + const page = Number(searchParams.get('page')) + const name = searchParams.get('name') + const setPage = (page: number) => { + searchParams.set('page', page.toString()) + setSearchParams(searchParams) + } + + const setName = (name: string) => { + if (name === '') { + searchParams.delete('name') + } else { + searchParams.set('name', name) + } + searchParams.set('page', '1') + setSearchParams(searchParams) + } + const { data, isLoading, error } = useGetDecksQuery({ + currentPage: page || 1, + itemsPerPage: 8, + name: name ?? undefined, + }) + + const [createDeck, { isLoading: isDeckBeingCreated }] = useCreateDeckMutation() + + if (isLoading) { + return
Loading...
+ } + + if (error) { + console.error(error) + + return
Error
+ } + + return ( + +
+ + Decks + + +
+
+ +
+ + +
+ + +
+ + + + Name + Cards + Last updated + Created by + + + + {data?.items.map(deck => { + return ( + + {deck.name} + {deck.cardsCount} + {new Date(deck.updated).toLocaleDateString()} + {deck.author.name} + + ) + })} + +
+ +
+ ) +} diff --git a/src/router.tsx b/src/router.tsx new file mode 100644 index 0000000..d0ee294 --- /dev/null +++ b/src/router.tsx @@ -0,0 +1,35 @@ +import { createBrowserRouter, Navigate, Outlet, RouterProvider } from 'react-router-dom' + +import { LoginPage } from '@/pages/auth/login/login.tsx' +import { Decks } from '@/pages/decks/decks.tsx' + +const publicRoutes = [ + { + path: '/login', + element: , + }, +] +const privateRoutes = [ + { + path: '/', + element: , + }, +] + +const router = createBrowserRouter([ + ...publicRoutes, + { + element: , + children: privateRoutes, + }, +]) + +export const Router = () => { + return +} + +function PrivateRoutes() { + const isAuthenticated = true + + return isAuthenticated ? : +} diff --git a/src/services/base-api.ts b/src/services/base-api.ts new file mode 100644 index 0000000..43a17da --- /dev/null +++ b/src/services/base-api.ts @@ -0,0 +1,14 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' + +export const baseApi = createApi({ + reducerPath: 'baseApi', + tagTypes: ['Decks'], + baseQuery: fetchBaseQuery({ + baseUrl: 'https://api.flashcards.andrii.es', + credentials: 'include', + prepareHeaders: headers => { + headers.append('x-auth-skip', 'true') + }, + }), + endpoints: () => ({}), +}) diff --git a/src/services/decks/decks.service.ts b/src/services/decks/decks.service.ts new file mode 100644 index 0000000..1625802 --- /dev/null +++ b/src/services/decks/decks.service.ts @@ -0,0 +1,40 @@ +import { baseApi } from '@/services/base-api.ts' +import { + CreateDeckArgs, + Deck, + GetDecksArgs, + GetDecksResponse, +} from '@/services/decks/decks.types.ts' + +export const decksService = baseApi.injectEndpoints({ + endpoints: builder => { + return { + getDecks: builder.query({ + query: params => { + return { + url: 'v1/decks', + params: params ?? {}, + } + }, + providesTags: ['Decks'], + }), + getDeckById: builder.query({ + query: ({ id }) => { + return { + url: `v1/decks/${id}`, + } + }, + }), + createDeck: builder.mutation({ + query: args => ({ + url: 'v1/decks', + method: 'POST', + body: args, + }), + invalidatesTags: ['Decks'], + }), + } + }, +}) + +export const { useGetDecksQuery, useGetDeckByIdQuery, useCreateDeckMutation } = decksService diff --git a/src/services/decks/decks.types.ts b/src/services/decks/decks.types.ts new file mode 100644 index 0000000..17897b0 --- /dev/null +++ b/src/services/decks/decks.types.ts @@ -0,0 +1,42 @@ +export type GetDecksResponse = { + maxCardsCount: number + pagination: Pagination + items: Deck[] +} +export type Pagination = { + totalPages: number + currentPage: number + itemsPerPage: number + totalItems: number +} +export type Author = { + id: string + name: string +} +export type Deck = { + id: string + userId: string + name: string + isPrivate: boolean + shots: number + cover: null | string + created: string + updated: string + cardsCount: number + author: Author +} + +export type GetDecksArgs = { + minCardsCount?: number + maxCardsCount?: number + name?: string + authorId?: string + orderBy?: string + currentPage?: number + itemsPerPage?: number +} + +export type CreateDeckArgs = { + name: string + isPrivate?: boolean +} diff --git a/src/services/store.ts b/src/services/store.ts new file mode 100644 index 0000000..f718d99 --- /dev/null +++ b/src/services/store.ts @@ -0,0 +1,10 @@ +import { configureStore } from '@reduxjs/toolkit' + +import { baseApi } from '@/services/base-api.ts' + +export const store = configureStore({ + reducer: { [baseApi.reducerPath]: baseApi.reducer }, + middleware: getDefaultMiddleware => getDefaultMiddleware().concat(baseApi.middleware), +}) +export type AppDispatch = typeof store.dispatch +export type RootState = ReturnType