├── README.md ├── .eslintignore ├── src ├── assets │ ├── index.ts │ └── icons │ │ ├── index.ts │ │ ├── components-manual │ │ ├── index.ts │ │ ├── check.tsx │ │ ├── keyboard-arrow-left.tsx │ │ ├── keyboard-arrow-right.tsx │ │ └── visibility-off.tsx │ │ ├── svgs │ │ ├── radio_button_unchecked.svg │ │ ├── radio_button_checked.svg │ │ ├── email.svg │ │ ├── more-horizontal.svg │ │ ├── more-horizontal-outline.svg │ │ ├── menu-outline.svg │ │ ├── more-vertical-outline.svg │ │ ├── bookmark.svg │ │ ├── heart.svg │ │ ├── arrow-ios-back.svg │ │ ├── email-outline.svg │ │ ├── arrow-ios-back-outline.svg │ │ ├── trash.svg │ │ ├── image.svg │ │ ├── home.svg │ │ ├── person.svg │ │ ├── home-outline.svg │ │ ├── paper-plane.svg │ │ ├── funnel-outline 1.svg │ │ ├── paper-plane-outline.svg │ │ ├── arrow-ios-forward.svg │ │ ├── arrow-ios-forward-outline.svg │ │ ├── copy.svg │ │ ├── arrow-ios-Up.svg │ │ ├── trash-outline.svg │ │ ├── arrow-ios-Down-outline.svg │ │ ├── edit-2.svg │ │ ├── pin.svg │ │ ├── image-outline.svg │ │ ├── arrow-back-outline.svg │ │ ├── eye.svg │ │ ├── plus-square.svg │ │ ├── bookmark-outline.svg │ │ ├── edit-2-outline.svg │ │ ├── arrow-forward-outline.svg │ │ ├── star.svg │ │ ├── Block.svg │ │ └── credit-card.svg │ │ └── components │ │ ├── Person.tsx │ │ ├── Email.tsx │ │ ├── Block.tsx │ │ ├── MoreHorizontal.tsx │ │ ├── PauseCircle.tsx │ │ ├── PersonRemove.tsx │ │ ├── PlusCircle.tsx │ │ ├── ArrowIosBack.tsx │ │ ├── Bookmark.tsx │ │ ├── Heart.tsx │ │ ├── ArrowIosUp.tsx │ │ ├── MoreHorizontalOutline.tsx │ │ ├── PersonAdd.tsx │ │ ├── PersonOutline.tsx │ │ ├── PlusSquare.tsx │ │ ├── ArrowIosForward.tsx │ │ ├── Search.tsx │ │ ├── LogOut.tsx │ │ ├── Trash.tsx │ │ ├── Pin.tsx │ │ ├── ArrowIosBackOutline.tsx │ │ ├── Edit2.tsx │ │ ├── Home.tsx │ │ ├── PlusCircleOutline.tsx │ │ ├── RadioButtonUnchecked.tsx │ │ ├── Image.tsx │ │ ├── ArrowIosDownOutline.tsx │ │ ├── CreditCard.tsx │ │ ├── EmailOutline.tsx │ │ ├── ArrowIosForwardOutline.tsx │ │ ├── SearchOutline.tsx │ │ ├── Close.tsx │ │ ├── LogOutOutline.tsx │ │ ├── Expand.tsx │ │ ├── Eye.tsx │ │ ├── Maximize.tsx │ │ ├── MoreVerticalOutline.tsx │ │ ├── Copy.tsx │ │ ├── PersonRemoveOutline.tsx │ │ ├── Star.tsx │ │ ├── RadioButtonChecked.tsx │ │ ├── HomeOutline.tsx │ │ ├── Mic.tsx │ │ ├── CloseOutline.tsx │ │ ├── PauseCircleOutline.tsx │ │ ├── ExpandOutline.tsx │ │ ├── PersonAddOutline.tsx │ │ ├── TrashOutline.tsx │ │ ├── ArrowBackOutline.tsx │ │ ├── Calendar.tsx │ │ ├── FunnelOutline1.tsx │ │ ├── PaperPlane.tsx │ │ ├── PlayCircle.tsx │ │ ├── Edit2Outline.tsx │ │ ├── MessageCircle.tsx │ │ ├── ArrowForwardOutline.tsx │ │ ├── CreditCardOutline.tsx │ │ ├── ImageOutline.tsx │ │ ├── PaperPlaneOutline.tsx │ │ ├── TrendingUp.tsx │ │ ├── PinOutline.tsx │ │ ├── PlusSquareOutline.tsx │ │ ├── MenuOutline.tsx │ │ ├── PlayCircleOutline.tsx │ │ ├── MaximizeOutline.tsx │ │ ├── TrendingUpOutline.tsx │ │ ├── BookmarkOutline.tsx │ │ ├── CalendarOutline.tsx │ │ ├── MicOutline.tsx │ │ ├── HeartOutline.tsx │ │ ├── EyeOutline.tsx │ │ ├── CopyOutline.tsx │ │ ├── StarOutline.tsx │ │ ├── MessageCircleOutline.tsx │ │ ├── Layers.tsx │ │ └── LayersOutline.tsx ├── components │ ├── ui │ │ ├── card │ │ │ ├── index.ts │ │ │ ├── card.module.scss │ │ │ ├── card.tsx │ │ │ └── card.stories.tsx │ │ ├── page │ │ │ ├── index.ts │ │ │ ├── page.module.scss │ │ │ └── page.tsx │ │ ├── tabs │ │ │ ├── index.ts │ │ │ ├── tabs.module.scss │ │ │ └── tabs.tsx │ │ ├── avatar │ │ │ ├── index.ts │ │ │ ├── avatar.module.scss │ │ │ ├── avatar.stories.tsx │ │ │ └── avatar.tsx │ │ ├── button │ │ │ ├── index.ts │ │ │ └── button.tsx │ │ ├── dialog │ │ │ ├── index.ts │ │ │ ├── dialog.module.scss │ │ │ ├── dialog.tsx │ │ │ └── dialog.stories.tsx │ │ ├── modal │ │ │ ├── index.ts │ │ │ ├── modal.module.scss │ │ │ ├── modal.stories.tsx │ │ │ └── modal.tsx │ │ ├── slider │ │ │ ├── index.ts │ │ │ └── slider.stories.tsx │ │ ├── table │ │ │ ├── index.ts │ │ │ └── table.module.scss │ │ ├── checkbox │ │ │ ├── index.ts │ │ │ └── checkbox.stories.tsx │ │ ├── dropdown │ │ │ └── index.ts │ │ ├── spinner │ │ │ ├── index.ts │ │ │ ├── spinner.stories.tsx │ │ │ ├── spinner.tsx │ │ │ └── spinner.module.scss │ │ ├── pagination │ │ │ ├── index.ts │ │ │ └── pagination.module.scss │ │ ├── radio-group │ │ │ ├── index.ts │ │ │ └── radio-group.stories.tsx │ │ ├── text-field │ │ │ ├── index.ts │ │ │ └── text-field.stories.ts │ │ ├── typography │ │ │ ├── index.ts │ │ │ └── typography.tsx │ │ ├── controlled │ │ │ ├── controlled-checkbox │ │ │ │ ├── index.ts │ │ │ │ └── controlled-checkbox.tsx │ │ │ ├── controlled-text-field │ │ │ │ ├── index.ts │ │ │ │ └── controlled-text-field.tsx │ │ │ ├── controlled-radio-group │ │ │ │ ├── index.ts │ │ │ │ └── controlled-radio-group.tsx │ │ │ └── index.ts │ │ ├── label │ │ │ ├── label.module.scss │ │ │ └── label.tsx │ │ └── index.ts │ ├── layout │ │ ├── index.ts │ │ ├── header │ │ │ ├── index.ts │ │ │ ├── user-dropdown │ │ │ │ ├── index.ts │ │ │ │ ├── user-dropdown.module.scss │ │ │ │ └── user-dropdown.stories.tsx │ │ │ ├── header.module.scss │ │ │ ├── header.stories.tsx │ │ │ └── header.tsx │ │ ├── layout.module.scss │ │ ├── layout.stories.tsx │ │ └── layout.tsx │ ├── auth │ │ ├── sign-in │ │ │ ├── index.ts │ │ │ ├── sign-in.stories.tsx │ │ │ └── sign-in.module.scss │ │ ├── sign-up │ │ │ ├── index.ts │ │ │ ├── sign-up.stories.tsx │ │ │ └── sign-up.module.scss │ │ ├── check-email │ │ │ ├── index.ts │ │ │ ├── check-email.stories.tsx │ │ │ ├── check-email.module.scss │ │ │ └── check-email.tsx │ │ ├── new-password │ │ │ ├── index.ts │ │ │ ├── new-password.stories.tsx │ │ │ └── new-password.module.scss │ │ ├── recover-password │ │ │ ├── index.ts │ │ │ ├── recover-password.stories.tsx │ │ │ └── recover-password.module.scss │ │ └── index.ts │ ├── decks │ │ ├── deck-dialog │ │ │ ├── index.ts │ │ │ └── deck-dialog.module.scss │ │ ├── delete-deck-dialog │ │ │ ├── index.ts │ │ │ ├── delete-deck-dialog.module.scss │ │ │ ├── delete-deck-dialog.tsx │ │ │ └── delete-deck-dialog.stories.tsx │ │ ├── index.ts │ │ ├── decks-table.module.scss │ │ └── cards-table.tsx │ ├── profile │ │ ├── index.ts │ │ └── personal-information │ │ │ ├── index.ts │ │ │ └── personal-information.stories.tsx │ └── index.ts ├── hooks │ ├── index.ts │ └── use-query-param │ │ ├── index.ts │ │ └── use-query-param.ts ├── pages │ ├── decks-page │ │ ├── index.ts │ │ └── decks-page.module.scss │ ├── sign-in-page │ │ ├── index.ts │ │ └── sign-in-page.tsx │ ├── index.ts │ └── deck-page │ │ └── deck-page.tsx ├── vite-env.d.ts ├── utils │ ├── date │ │ ├── index.ts │ │ └── format-date.ts │ ├── index.ts │ ├── get-valuable.ts │ └── merge-refs.ts ├── services │ ├── index.ts │ ├── decks │ │ ├── index.ts │ │ └── decks.selectors.ts │ ├── auth │ │ ├── auth.types.ts │ │ └── auth.service.ts │ ├── base-api.ts │ ├── store.ts │ └── base-query-with-reauth.ts ├── styles │ ├── index.scss │ ├── _typography.scss │ ├── _mixins.scss │ ├── _tokens.scss │ └── _colors.scss ├── main.tsx ├── App.tsx └── router.tsx ├── .prettierrc.cjs ├── .stylelintrc.cjs ├── tsconfig.node.json ├── vite.config.ts ├── .gitignore ├── eslint ├── index.js └── no-wrong-redux-import.js ├── index.html ├── .eslintrc.cjs ├── .github └── workflows │ └── storybook.yml ├── .storybook └── main.ts └── tsconfig.json /README.md: -------------------------------------------------------------------------------- 1 | this is a very new readme -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /src/assets/icons/components/ -------------------------------------------------------------------------------- /src/assets/index.ts: -------------------------------------------------------------------------------- 1 | export * from './icons' 2 | -------------------------------------------------------------------------------- /src/components/ui/card/index.ts: -------------------------------------------------------------------------------- 1 | export * from './card' 2 | -------------------------------------------------------------------------------- /src/components/ui/page/index.ts: -------------------------------------------------------------------------------- 1 | export * from './page' 2 | -------------------------------------------------------------------------------- /src/components/ui/tabs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tabs' 2 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-query-param' 2 | -------------------------------------------------------------------------------- /src/components/layout/index.ts: -------------------------------------------------------------------------------- 1 | export * from './layout' 2 | -------------------------------------------------------------------------------- /src/components/ui/avatar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './avatar' 2 | -------------------------------------------------------------------------------- /src/components/ui/button/index.ts: -------------------------------------------------------------------------------- 1 | export * from './button' 2 | -------------------------------------------------------------------------------- /src/components/ui/dialog/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dialog' 2 | -------------------------------------------------------------------------------- /src/components/ui/modal/index.ts: -------------------------------------------------------------------------------- 1 | export * from './modal' 2 | -------------------------------------------------------------------------------- /src/components/ui/slider/index.ts: -------------------------------------------------------------------------------- 1 | export * from './slider' 2 | -------------------------------------------------------------------------------- /src/components/ui/table/index.ts: -------------------------------------------------------------------------------- 1 | export * from './table' 2 | -------------------------------------------------------------------------------- /src/pages/decks-page/index.ts: -------------------------------------------------------------------------------- 1 | export * from './decks-page' 2 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/components/auth/sign-in/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sign-in' 2 | -------------------------------------------------------------------------------- /src/components/auth/sign-up/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sign-up' 2 | -------------------------------------------------------------------------------- /src/components/layout/header/index.ts: -------------------------------------------------------------------------------- 1 | export * from './header' 2 | -------------------------------------------------------------------------------- /src/components/ui/checkbox/index.ts: -------------------------------------------------------------------------------- 1 | export * from './checkbox' 2 | -------------------------------------------------------------------------------- /src/components/ui/dropdown/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dropdown' 2 | -------------------------------------------------------------------------------- /src/components/ui/spinner/index.ts: -------------------------------------------------------------------------------- 1 | export * from './spinner' 2 | -------------------------------------------------------------------------------- /src/pages/sign-in-page/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sign-in-page' 2 | -------------------------------------------------------------------------------- /src/components/ui/pagination/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pagination' 2 | -------------------------------------------------------------------------------- /src/components/ui/radio-group/index.ts: -------------------------------------------------------------------------------- 1 | export * from './radio-group' 2 | -------------------------------------------------------------------------------- /src/components/ui/text-field/index.ts: -------------------------------------------------------------------------------- 1 | export * from './text-field' 2 | -------------------------------------------------------------------------------- /src/components/ui/typography/index.ts: -------------------------------------------------------------------------------- 1 | export * from './typography' 2 | -------------------------------------------------------------------------------- /src/hooks/use-query-param/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-query-param' 2 | -------------------------------------------------------------------------------- /src/utils/date/index.ts: -------------------------------------------------------------------------------- 1 | export { formatDate } from './format-date' 2 | -------------------------------------------------------------------------------- /src/components/auth/check-email/index.ts: -------------------------------------------------------------------------------- 1 | export * from './check-email' 2 | -------------------------------------------------------------------------------- /src/components/auth/new-password/index.ts: -------------------------------------------------------------------------------- 1 | export * from './new-password' 2 | -------------------------------------------------------------------------------- /src/components/decks/deck-dialog/index.ts: -------------------------------------------------------------------------------- 1 | export * from './deck-dialog' 2 | -------------------------------------------------------------------------------- /src/components/profile/index.ts: -------------------------------------------------------------------------------- 1 | export * from './personal-information' 2 | -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base-api' 2 | export * from './decks' 3 | -------------------------------------------------------------------------------- /src/components/auth/recover-password/index.ts: -------------------------------------------------------------------------------- 1 | export * from './recover-password' 2 | -------------------------------------------------------------------------------- /src/components/layout/header/user-dropdown/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user-dropdown' 2 | -------------------------------------------------------------------------------- /src/pages/index.ts: -------------------------------------------------------------------------------- 1 | export * from './decks-page' 2 | export * from './sign-in-page' 3 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('@it-incubator/prettier-config'), 3 | } -------------------------------------------------------------------------------- /src/components/decks/delete-deck-dialog/index.ts: -------------------------------------------------------------------------------- 1 | export * from './delete-deck-dialog' 2 | -------------------------------------------------------------------------------- /src/assets/icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components' 2 | export * from './components-manual' 3 | -------------------------------------------------------------------------------- /src/components/decks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cards-table' 2 | export * from './decks-table' 3 | -------------------------------------------------------------------------------- /src/components/profile/personal-information/index.ts: -------------------------------------------------------------------------------- 1 | export * from './personal-information' 2 | -------------------------------------------------------------------------------- /src/services/decks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './decks.service' 2 | export * from './decks.types' 3 | -------------------------------------------------------------------------------- /src/components/ui/controlled/controlled-checkbox/index.ts: -------------------------------------------------------------------------------- 1 | export * from './controlled-checkbox' 2 | -------------------------------------------------------------------------------- /src/components/ui/controlled/controlled-text-field/index.ts: -------------------------------------------------------------------------------- 1 | export * from './controlled-text-field' 2 | -------------------------------------------------------------------------------- /src/components/ui/controlled/controlled-radio-group/index.ts: -------------------------------------------------------------------------------- 1 | export * from './controlled-radio-group' 2 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './date' 2 | export * from './get-valuable' 3 | export * from './merge-refs' 4 | -------------------------------------------------------------------------------- /src/components/ui/avatar/avatar.module.scss: -------------------------------------------------------------------------------- 1 | .avatar { 2 | object-fit: cover; 3 | border-radius: 9999px; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth' 2 | export * from './decks' 3 | export * from './profile' 4 | export * from './ui' 5 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @forward 'mixins'; 2 | @forward 'colors'; 3 | @forward 'typography'; 4 | @forward 'boilerplate'; 5 | @forward 'tokens'; 6 | -------------------------------------------------------------------------------- /src/components/ui/page/page.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | justify-content: center; 4 | width: 100%; 5 | margin-top: 36px; 6 | } 7 | -------------------------------------------------------------------------------- /.stylelintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@it-incubator/stylelint-config', 3 | rules: { 4 | 'value-keyword-case': null, 5 | } 6 | } -------------------------------------------------------------------------------- /src/components/ui/controlled/index.ts: -------------------------------------------------------------------------------- 1 | export * from './controlled-checkbox' 2 | export * from './controlled-radio-group' 3 | export * from './controlled-text-field' 4 | -------------------------------------------------------------------------------- /src/components/decks/delete-deck-dialog/delete-deck-dialog.module.scss: -------------------------------------------------------------------------------- 1 | .content { 2 | padding: 18px 24px; 3 | 4 | > p { 5 | margin: 0; 6 | padding: 0; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/components/ui/dialog/dialog.module.scss: -------------------------------------------------------------------------------- 1 | .buttons { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | padding: 12px 24px 36px; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/decks/deck-dialog/deck-dialog.module.scss: -------------------------------------------------------------------------------- 1 | .content { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 24px; 5 | 6 | width: 100%; 7 | padding: 24px; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from './check-email' 2 | export * from './new-password' 3 | export * from './recover-password' 4 | export * from './sign-in' 5 | export * from './sign-up' 6 | -------------------------------------------------------------------------------- /src/utils/date/format-date.ts: -------------------------------------------------------------------------------- 1 | export function formatDate(date: string | undefined) { 2 | if (!date) { 3 | return '' 4 | } 5 | 6 | return new Date(date).toLocaleDateString('ru-RU') 7 | } 8 | -------------------------------------------------------------------------------- /src/components/ui/card/card.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | background-color: var(--color-dark-700); 3 | border-radius: 2px; 4 | box-shadow: 5 | 1px 1px 2px rgb(0 0 0 / 10%), 6 | -1px -1px 2px rgb(0 0 0 / 10%); 7 | } 8 | -------------------------------------------------------------------------------- /src/components/ui/label/label.module.scss: -------------------------------------------------------------------------------- 1 | .label { 2 | display: inline-block; 3 | 4 | margin-bottom: 1px; 5 | 6 | font-size: var(--font-size-s); 7 | line-height: var(--line-height-m); 8 | color: var(var(--color-light-100)); 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/components/layout/header/user-dropdown/user-dropdown.module.scss: -------------------------------------------------------------------------------- 1 | .email { 2 | color: var(--color-dark-100); 3 | } 4 | 5 | .trigger { 6 | gap: 14px; 7 | align-items: center; 8 | } 9 | 10 | .name { 11 | text-decoration: underline dashed var(--color-light-100) 1px; 12 | text-underline-offset: 6px; 13 | } 14 | -------------------------------------------------------------------------------- /src/services/auth/auth.types.ts: -------------------------------------------------------------------------------- 1 | export type LoginArgs = { 2 | email: string 3 | password: string 4 | rememberMe?: boolean 5 | } 6 | export type User = { 7 | avatar: null | string 8 | created: string 9 | email: string 10 | id: string 11 | isEmailVerified: boolean 12 | name: string 13 | updated: string 14 | } 15 | -------------------------------------------------------------------------------- /src/components/layout/layout.module.scss: -------------------------------------------------------------------------------- 1 | .layout { 2 | width: 100%; 3 | padding: var(--header-height) var(--horizontal-padding) 0; 4 | } 5 | 6 | .content { 7 | display: flex; 8 | align-items: center; 9 | justify-content: space-between; 10 | 11 | width: 100%; 12 | max-width: var(--max-width); 13 | margin: 0 auto; 14 | } 15 | -------------------------------------------------------------------------------- /src/services/base-api.ts: -------------------------------------------------------------------------------- 1 | import { baseQueryWithReauth } from '@/services/base-query-with-reauth' 2 | import { createApi } from '@reduxjs/toolkit/query/react' 3 | 4 | export const baseApi = createApi({ 5 | baseQuery: baseQueryWithReauth, 6 | endpoints: () => ({}), 7 | reducerPath: 'baseApi', 8 | tagTypes: ['Decks', 'Me'], 9 | }) 10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | 3 | import react from '@vitejs/plugin-react' 4 | import { defineConfig } from 'vite' 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [react()], 9 | resolve: { 10 | alias: [{ find: '@', replacement: path.resolve(__dirname, 'src') }], 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /eslint/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const ruleFiles = fs 5 | .readdirSync(__dirname) 6 | .filter(file => file !== 'index.js' && !file.endsWith('test.js')) 7 | 8 | const rules = Object.fromEntries( 9 | ruleFiles.map(file => [path.basename(file, '.js'), require('./' + file)]) 10 | ) 11 | 12 | module.exports = { rules } 13 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | 3 | import { createRoot } from 'react-dom/client' 4 | 5 | import './styles/index.scss' 6 | import '@fontsource/roboto/400.css' 7 | import '@fontsource/roboto/700.css' 8 | 9 | import { App } from './App' 10 | 11 | createRoot(document.getElementById('root')!).render( 12 | 13 | 14 | 15 | ) 16 | -------------------------------------------------------------------------------- /src/utils/get-valuable.ts: -------------------------------------------------------------------------------- 1 | type Valuable = { [K in keyof T as T[K] extends null | undefined ? never : K]: T[K] } 2 | 3 | export function getValuable>(obj: T): V { 4 | return Object.fromEntries( 5 | Object.entries(obj).filter( 6 | ([, v]) => !((typeof v === 'string' && !v.length) || v === null || typeof v === 'undefined') 7 | ) 8 | ) as V 9 | } 10 | -------------------------------------------------------------------------------- /src/pages/decks-page/decks-page.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | width: 100%; 3 | } 4 | 5 | .header { 6 | display: flex; 7 | align-items: center; 8 | justify-content: space-between; 9 | width: 100%; 10 | } 11 | 12 | .filters { 13 | display: flex; 14 | grid-template-columns: repeat(4, 1fr); 15 | column-gap: 16px; 16 | margin: 10px 0 16px; 17 | } 18 | 19 | .pagination { 20 | margin-top: 24px; 21 | } 22 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Provider } from 'react-redux' 2 | import { ToastContainer } from 'react-toastify' 3 | 4 | import { Router } from '@/router' 5 | import { store } from '@/services/store' 6 | 7 | import 'react-toastify/dist/ReactToastify.css' 8 | 9 | export function App() { 10 | return ( 11 | 12 | 13 | 14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@it-incubator/eslint-config', 'plugin:storybook/recommended'], 3 | plugins: ['myPlugin'], 4 | rules: { 5 | 'myPlugin/no-wrong-redux-import': 'error', 6 | }, 7 | overrides: [ 8 | { 9 | files: ['**/*.stories.tsx'], 10 | rules: { 11 | 'react-hooks/rules-of-hooks': 'off', 12 | 'no-console': 'off', 13 | }, 14 | }, 15 | ], 16 | } 17 | -------------------------------------------------------------------------------- /src/components/ui/slider/slider.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Slider } from './' 4 | 5 | const meta = { 6 | component: Slider, 7 | parameters: {}, 8 | tags: ['autodocs'], 9 | title: 'Components/Slider', 10 | } satisfies Meta 11 | 12 | export default meta 13 | type Story = StoryObj 14 | 15 | export const Default: Story = { 16 | args: { value: [0, 100] }, 17 | } 18 | -------------------------------------------------------------------------------- /src/components/auth/sign-in/sign-in.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { SignIn } from './' 4 | 5 | const meta = { 6 | component: SignIn, 7 | tags: ['autodocs'], 8 | title: 'Auth/Sign in', 9 | } satisfies Meta 10 | 11 | export default meta 12 | type Story = StoryObj 13 | 14 | export const Default: Story = { 15 | args: { 16 | onSubmit: data => console.info(data), 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /src/components/auth/sign-up/sign-up.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { SignUp } from './' 4 | 5 | const meta = { 6 | component: SignUp, 7 | tags: ['autodocs'], 8 | title: 'Auth/Sign up', 9 | } satisfies Meta 10 | 11 | export default meta 12 | type Story = StoryObj 13 | 14 | export const Default: Story = { 15 | args: { 16 | onSubmit: data => console.info(data), 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /src/components/ui/spinner/spinner.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Spinner } from './' 4 | 5 | const meta = { 6 | component: Spinner, 7 | parameters: { 8 | layout: 'centered', 9 | }, 10 | tags: ['autodocs'], 11 | title: 'Components/Spinner', 12 | } satisfies Meta 13 | 14 | export default meta 15 | type Story = StoryObj 16 | 17 | export const Default: Story = { 18 | args: {}, 19 | } 20 | -------------------------------------------------------------------------------- /src/components/auth/check-email/check-email.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { CheckEmail } from './' 4 | 5 | const meta = { 6 | component: CheckEmail, 7 | tags: ['autodocs'], 8 | title: 'Auth/Check email', 9 | } satisfies Meta 10 | 11 | export default meta 12 | type Story = StoryObj 13 | 14 | export const Default: Story = { 15 | args: { 16 | email: 'your_email@domain.com', 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /src/components/ui/avatar/avatar.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Avatar } from './' 4 | 5 | const meta = { 6 | component: Avatar, 7 | tags: ['autodocs'], 8 | title: 'Components/Avatar', 9 | } satisfies Meta 10 | 11 | export default meta 12 | type Story = StoryObj 13 | 14 | export const Default: Story = { 15 | args: { 16 | src: 'https://avatars.githubusercontent.com/u/1196870?v=4', 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /src/components/auth/new-password/new-password.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { NewPassword } from './' 4 | 5 | const meta = { 6 | component: NewPassword, 7 | tags: ['autodocs'], 8 | title: 'Auth/New password', 9 | } satisfies Meta 10 | 11 | export default meta 12 | type Story = StoryObj 13 | 14 | export const Default: Story = { 15 | args: { 16 | onSubmit: data => console.info(data), 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /src/components/ui/page/page.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithoutRef, forwardRef } from 'react' 2 | 3 | import { clsx } from 'clsx' 4 | 5 | import s from './page.module.scss' 6 | 7 | export type PageProps = ComponentPropsWithoutRef<'div'> 8 | 9 | export const Page = forwardRef(({ className, ...props }, ref) => { 10 | const classNames = { 11 | root: clsx(s.root, className), 12 | } 13 | 14 | return 15 | }) 16 | -------------------------------------------------------------------------------- /src/components/decks/decks-table.module.scss: -------------------------------------------------------------------------------- 1 | .iconsContainer { 2 | display: flex; 3 | gap: 10px; 4 | align-items: center; 5 | } 6 | 7 | .nameContainer { 8 | display: flex; 9 | align-items: center; 10 | gap: 10px; 11 | 12 | img { 13 | width: 7.5rem; 14 | height: 3rem; 15 | object-fit: cover; 16 | } 17 | } 18 | 19 | .favoriteToggleContainer { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | 25 | .favoriteToggle { 26 | height: 100%; 27 | } 28 | -------------------------------------------------------------------------------- /src/components/ui/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../layout/header' 2 | export * from './avatar' 3 | export * from './button' 4 | export * from './card' 5 | export * from './checkbox' 6 | export * from './controlled' 7 | export * from './dialog' 8 | export * from './dropdown' 9 | export * from './modal' 10 | export * from './page' 11 | export * from './radio-group' 12 | export * from './slider' 13 | export * from './spinner' 14 | export * from './table' 15 | export * from './text-field' 16 | export * from './typography' 17 | -------------------------------------------------------------------------------- /src/styles/_typography.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --font-family-primary: 'Roboto', sans-serif; 3 | 4 | // line heights 5 | --line-height-s: 16px; 6 | --line-height-m: 24px; 7 | --line-height-l: 36px; 8 | 9 | // font sizes 10 | --font-size-xs: 0.75rem; 11 | --font-size-s: 0.875rem; 12 | --font-size-m: 1rem; 13 | --font-size-l: 1.125rem; 14 | --font-size-xl: 1.25rem; 15 | --font-size-xxl: 1.625rem; 16 | 17 | // font weights 18 | --font-weight-regular: 400; 19 | --font-weight-bold: 700; 20 | } 21 | -------------------------------------------------------------------------------- /src/components/ui/card/card.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithoutRef, forwardRef } from 'react' 2 | 3 | import { clsx } from 'clsx' 4 | 5 | import s from './card.module.scss' 6 | 7 | export type CardProps = {} & ComponentPropsWithoutRef<'div'> 8 | 9 | export const Card = forwardRef(({ className, ...restProps }, ref) => { 10 | const classNames = { 11 | root: clsx(s.root, className), 12 | } 13 | 14 | return 15 | }) 16 | -------------------------------------------------------------------------------- /src/components/auth/recover-password/recover-password.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { RecoverPassword } from './' 4 | 5 | const meta = { 6 | component: RecoverPassword, 7 | tags: ['autodocs'], 8 | title: 'Auth/Recover password', 9 | } satisfies Meta 10 | 11 | export default meta 12 | type Story = StoryObj 13 | 14 | export const Default: Story = { 15 | args: { 16 | onSubmit: data => console.info(data), 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /src/styles/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin sm-up { 2 | @media screen and (width > 600px) { 3 | @content; 4 | } 5 | } 6 | 7 | @mixin md-up { 8 | @media screen and (width > 768px) { 9 | @content; 10 | } 11 | } 12 | 13 | @mixin lg-up { 14 | @media screen and (width > 1024px) { 15 | @content; 16 | } 17 | } 18 | 19 | @mixin xl-up { 20 | @media screen and (width > 1280px) { 21 | @content; 22 | } 23 | } 24 | 25 | @mixin xxl-up { 26 | @media screen and (width > 1536px) { 27 | @content; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/assets/icons/components-manual/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Camera } from './camera' 2 | export { default as Check } from './check' 3 | export { default as ChevronUp } from './chevron-up' 4 | export { default as Edit } from './edit' 5 | export { default as KeyboardArrowLeft } from './keyboard-arrow-left' 6 | export { default as KeyboardArrowRight } from './keyboard-arrow-right' 7 | export { default as Logo } from './logo' 8 | export { default as Logout } from './logout' 9 | export { default as VisibilityOff } from './visibility-off' 10 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/radio_button_unchecked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/storybook.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy Storybook 2 | on: 3 | push: 4 | branches: 5 | - 'master' 6 | jobs: 7 | build-and-deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 🛎️ 11 | uses: actions/checkout@v2.3.1 12 | 13 | - name: Merge master -> storybook-deploy 🚀 14 | uses: devmasx/merge-branch@1.4.0 15 | with: 16 | type: now 17 | from_branch: master 18 | target_branch: storybook-deploy 19 | github_token: ${{ github.token }} -------------------------------------------------------------------------------- /src/components/layout/header/header.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | position: fixed; 3 | z-index: 100; 4 | top: 0; 5 | right: 0; 6 | left: 0; 7 | 8 | width: 100%; 9 | height: var(--header-height); 10 | padding: 12px var(--horizontal-padding); 11 | 12 | background: var(--color-dark-700); 13 | border-bottom: 1px solid var(--color-dark-500); 14 | } 15 | 16 | .content { 17 | display: flex; 18 | align-items: center; 19 | justify-content: space-between; 20 | 21 | width: 100%; 22 | max-width: var(--max-width); 23 | margin: 0 auto; 24 | } 25 | -------------------------------------------------------------------------------- /src/components/auth/check-email/check-email.module.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | max-width: 413px; 3 | padding: 35px 33px 48px; 4 | } 5 | 6 | .title { 7 | margin-bottom: 27px; 8 | text-align: center; 9 | } 10 | 11 | .iconContainer { 12 | display: flex; 13 | justify-content: center; 14 | width: 100%; 15 | margin-bottom: 19px; 16 | } 17 | 18 | .instructions { 19 | margin-bottom: 65px; 20 | color: var(--color-light-900); 21 | text-align: center; 22 | opacity: 0.5; 23 | } 24 | 25 | .signInLink { 26 | display: flex; 27 | justify-content: center; 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/merge-refs.ts: -------------------------------------------------------------------------------- 1 | //https://github.com/gregberge/react-merge-refs/blob/main/src/index.tsx 2 | 3 | import { LegacyRef, MutableRefObject, RefCallback } from 'react' 4 | 5 | export function mergeRefs( 6 | refs: Array | MutableRefObject | null | undefined> 7 | ): RefCallback { 8 | return value => { 9 | refs.forEach(ref => { 10 | if (typeof ref === 'function') { 11 | ref(value) 12 | } else if (ref != null) { 13 | ;(ref as MutableRefObject).current = value 14 | } 15 | }) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/styles/_tokens.scss: -------------------------------------------------------------------------------- 1 | @use 'mixins' as *; 2 | 3 | :root { 4 | /* outlines */ 5 | --color-outline-focus: var(--color-info-900); 6 | --outline-focus: 2px solid var(--color-outline-focus); 7 | 8 | /* transitions */ 9 | --transition-duration-basic: 200ms; 10 | 11 | /* border-radius */ 12 | --border-radius-s: 0.25rem; 13 | 14 | /* header */ 15 | --header-height: 60px; 16 | 17 | /* max-width */ 18 | --horizontal-padding: 16px; 19 | --max-width: calc(1005px + var(--horizontal-padding) * 2); 20 | 21 | @include md-up { 22 | --horizontal-padding: 24px; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/services/decks/decks.selectors.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from '@/services/store' 2 | 3 | export const selectDecksCurrentPage = (state: RootState) => state.decks.currentPage 4 | 5 | export const selectDecksPerPage = (state: RootState) => state.decks.perPage 6 | 7 | export const selectDecksSearch = (state: RootState) => state.decks.search 8 | 9 | export const selectDecksCurrentTab = (state: RootState) => state.decks.currentTab 10 | 11 | export const selectDecksMinCards = (state: RootState) => state.decks.minCards 12 | 13 | export const selectDecksMaxCards = (state: RootState) => state.decks.maxCards 14 | -------------------------------------------------------------------------------- /src/components/ui/avatar/avatar.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, ComponentPropsWithoutRef } from 'react' 2 | 3 | import clsx from 'clsx' 4 | 5 | import s from './avatar.module.scss' 6 | 7 | export type AvatarProps = { 8 | size?: CSSProperties['width'] 9 | } & ComponentPropsWithoutRef<'img'> 10 | 11 | export const Avatar = ({ className, size = '36px', style, ...rest }: AvatarProps) => { 12 | return ( 13 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/radio_button_checked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/ui/card/card.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Typography } from '@/components' 4 | 5 | import { Card } from './' 6 | 7 | const meta = { 8 | component: Card, 9 | tags: ['autodocs'], 10 | title: 'Components/Card', 11 | } satisfies Meta 12 | 13 | export default meta 14 | type Story = StoryObj 15 | 16 | export const Default: Story = { 17 | args: { 18 | children: Card, 19 | style: { 20 | height: '300px', 21 | padding: '24px', 22 | width: '300px', 23 | }, 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /src/services/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { baseApi } from '..' 2 | import { LoginArgs, User } from './auth.types' 3 | 4 | export const authService = baseApi.injectEndpoints({ 5 | endpoints: builder => ({ 6 | login: builder.mutation({ 7 | invalidatesTags: ['Me'], 8 | query: body => ({ 9 | body, 10 | method: 'POST', 11 | url: '/v1/auth/login', 12 | }), 13 | }), 14 | me: builder.query({ 15 | providesTags: ['Me'], 16 | query: () => '/v1/auth/me', 17 | }), 18 | }), 19 | }) 20 | 21 | export const { useLoginMutation, useMeQuery } = authService 22 | -------------------------------------------------------------------------------- /src/components/auth/new-password/new-password.module.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | max-width: 413px; 3 | padding: 33px 36px 60px; 4 | } 5 | 6 | .form { 7 | display: flex; 8 | flex-direction: column; 9 | gap: 24px; 10 | 11 | width: 100%; 12 | margin-bottom: 26px; 13 | } 14 | 15 | .title { 16 | margin-bottom: 51px; 17 | text-align: center; 18 | } 19 | 20 | .input { 21 | margin-bottom: 19px; 22 | } 23 | 24 | .instructions { 25 | margin-bottom: 41px; 26 | color: var(--color-light-900); 27 | opacity: 0.5; 28 | } 29 | 30 | .caption { 31 | margin-bottom: 11px; 32 | text-align: center; 33 | } 34 | 35 | .signInLink { 36 | display: flex; 37 | justify-content: center; 38 | } 39 | -------------------------------------------------------------------------------- /src/assets/icons/components-manual/check.tsx: -------------------------------------------------------------------------------- 1 | import { Ref, SVGProps, forwardRef, memo } from 'react' 2 | const SvgComponent = (props: SVGProps, ref: Ref) => ( 3 | 12 | 18 | 19 | ) 20 | const ForwardRef = forwardRef(SvgComponent) 21 | 22 | export default memo(ForwardRef) 23 | -------------------------------------------------------------------------------- /src/components/decks/delete-deck-dialog/delete-deck-dialog.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog, DialogProps } from '@/components' 2 | 3 | import s from './delete-deck-dialog.module.scss' 4 | export default {} 5 | type Props = { 6 | deckName: string 7 | } & Pick 8 | export const DeleteDeckDialog = ({ deckName, ...dialogProps }: Props) => { 9 | return ( 10 | 11 | 12 | 13 | Do you really want to remove {deckName}? 14 | 15 | All cards will be deleted. 16 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/components/ui/label/label.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithoutRef, FC, ReactNode } from 'react' 2 | 3 | import * as LabelRadixUI from '@radix-ui/react-label' 4 | import { clsx } from 'clsx' 5 | 6 | import s from './label.module.scss' 7 | 8 | export type LabelProps = { 9 | label?: ReactNode 10 | } & ComponentPropsWithoutRef<'label'> 11 | 12 | export const Label: FC = ({ children, className, label, ...rest }) => { 13 | const classNames = { 14 | label: clsx(s.label, className), 15 | } 16 | 17 | return ( 18 | 19 | {label && {label}} 20 | {children} 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/components/auth/sign-up/sign-up.module.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | width: 100%; 3 | max-width: 413px; 4 | padding: 33px 36px 29px; 5 | } 6 | 7 | .form { 8 | display: flex; 9 | flex-direction: column; 10 | gap: 24px; 11 | 12 | width: 100%; 13 | margin-bottom: 60px; 14 | } 15 | 16 | .title { 17 | margin-bottom: 27px; 18 | text-align: center; 19 | } 20 | 21 | .button { 22 | margin-bottom: 20px; 23 | } 24 | 25 | .caption { 26 | margin-bottom: 11px; 27 | color: var(--color-light-900); 28 | text-align: center; 29 | } 30 | 31 | .signInLink { 32 | display: flex; 33 | justify-content: center; 34 | 35 | font-size: var(--font-size-m); 36 | font-weight: var(--font-weight-bold); 37 | color: var(--color-accent-500); 38 | } 39 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/email.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/more-horizontal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/components/ui/tabs/tabs.module.scss: -------------------------------------------------------------------------------- 1 | .list { 2 | display: inline-flex; 3 | flex-shrink: 0; 4 | background-color: var(--color-dark-900); 5 | } 6 | 7 | .trigger { 8 | cursor: pointer; 9 | 10 | display: inline-flex; 11 | flex-shrink: 0; 12 | flex-wrap: nowrap; 13 | 14 | padding: 6px 24px; 15 | 16 | background-color: var(--color-dark-900); 17 | border: 1px solid var(--color-dark-300); 18 | 19 | &:last-of-type { 20 | border-radius: 0 2px 2px 0; 21 | } 22 | 23 | &:first-of-type { 24 | border-radius: 2px 0 0 2px; 25 | } 26 | 27 | &[data-state='active'] { 28 | background-color: var(--color-accent-500); 29 | border-color: var(--color-accent-500); 30 | } 31 | } 32 | 33 | .content { 34 | color: inherit; 35 | } 36 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/more-horizontal-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-vite' 2 | 3 | const config: StorybookConfig = { 4 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], 5 | addons: [ 6 | '@storybook/addon-links', 7 | '@storybook/addon-essentials', 8 | '@storybook/addon-onboarding', 9 | '@storybook/addon-interactions', 10 | 'storybook-addon-react-router-v6', 11 | ], 12 | framework: { 13 | name: '@storybook/react-vite', 14 | options: {}, 15 | }, 16 | docs: { 17 | autodocs: 'tag', 18 | }, 19 | viteFinal: async config => { 20 | config.assetsInclude = ['/sb-preview/runtime.js'] 21 | return config 22 | }, 23 | } 24 | export default config 25 | -------------------------------------------------------------------------------- /src/components/auth/recover-password/recover-password.module.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | max-width: 413px; 3 | padding: 33px 36px 29px; 4 | } 5 | 6 | .form { 7 | width: 100%; 8 | margin-bottom: 19px; 9 | } 10 | 11 | .title { 12 | margin-bottom: 50px; 13 | text-align: center; 14 | } 15 | 16 | .button { 17 | margin-bottom: 31px; 18 | } 19 | 20 | .instructions { 21 | margin-bottom: 65px; 22 | color: var(--color-light-900); 23 | opacity: 0.5; 24 | } 25 | 26 | .caption { 27 | margin-bottom: 11px; 28 | color: var(--color-light-900); 29 | text-align: center; 30 | } 31 | 32 | .loginLink { 33 | display: flex; 34 | justify-content: center; 35 | 36 | font-size: var(--font-size-m); 37 | font-weight: var(--font-weight-bold); 38 | color: var(--color-accent-500); 39 | } 40 | -------------------------------------------------------------------------------- /src/components/layout/header/user-dropdown/user-dropdown.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { UserDropdown } from './' 4 | 5 | const meta = { 6 | argTypes: { 7 | onLogout: { action: 'logout' }, 8 | }, 9 | component: UserDropdown, 10 | parameters: { 11 | layout: 'centered', 12 | }, 13 | tags: ['autodocs'], 14 | title: 'Components/UserDropdown', 15 | } satisfies Meta 16 | 17 | export default meta 18 | type Story = StoryObj 19 | 20 | export const Default: Story = { 21 | // @ts-expect-error onLogout is required but it is provided through argTypes 22 | args: { 23 | avatar: 'https://avatars.githubusercontent.com/u/1196870?v=4', 24 | email: 'johndoe@gmail.com', 25 | userName: 'John Doe', 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /src/components/ui/dialog/dialog.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Modal, ModalProps } from '@/components' 2 | 3 | import s from './dialog.module.scss' 4 | 5 | export type DialogProps = { 6 | cancelText?: string 7 | confirmText?: string 8 | onCancel?: () => void 9 | onConfirm?: () => void 10 | } & ModalProps 11 | export const Dialog = ({ 12 | cancelText = 'Cancel', 13 | children, 14 | confirmText = 'OK', 15 | onCancel, 16 | onConfirm, 17 | ...modalProps 18 | }: DialogProps) => { 19 | return ( 20 | 21 | {children} 22 | 23 | 24 | {cancelText} 25 | 26 | {confirmText} 27 | 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/pages/sign-in-page/sign-in-page.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom' 2 | import { toast } from 'react-toastify' 3 | 4 | import { Page, SignIn } from '@/components' 5 | import { useLoginMutation } from '@/services/auth/auth.service' 6 | import { LoginArgs } from '@/services/auth/auth.types' 7 | 8 | export const SignInPage = () => { 9 | const [signIn] = useLoginMutation() 10 | const navigate = useNavigate() 11 | const handleSignIn = async (data: LoginArgs) => { 12 | try { 13 | await signIn(data).unwrap() 14 | navigate('/') 15 | } catch (error: any) { 16 | console.log(error) 17 | toast.error(error?.data?.message ?? 'Could not sign in') 18 | } 19 | } 20 | 21 | return ( 22 | 23 | 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /eslint/no-wrong-redux-import.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** @type {import('eslint').Rule.RuleModule} */ 3 | module.exports = { 4 | create(context) { 5 | return { 6 | ImportDeclaration(node) { 7 | if ( 8 | (node.source && node.source.value === '@reduxjs/toolkit/query/') || 9 | node.source.value === '@reduxjs/toolkit/query' 10 | ) { 11 | context.report({ 12 | fix(fixer) { 13 | return fixer.replaceText(node.source, "'@reduxjs/toolkit/query/react'") 14 | }, 15 | message: 16 | "Import from '@reduxjs/toolkit/query/' is disallowed. Please import from '@reduxjs/toolkit/query/react'.", 17 | node, 18 | }) 19 | } 20 | }, 21 | } 22 | }, 23 | meta: { 24 | fixable: 'code', 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ui/modal/modal.module.scss: -------------------------------------------------------------------------------- 1 | .overlay { 2 | position: fixed; 3 | inset: 0; 4 | background-color: rgb(0 0 0 / 70%); 5 | } 6 | 7 | .content { 8 | position: fixed; 9 | top: 50%; 10 | left: 50%; 11 | transform: translate(-50%, -50%); 12 | 13 | width: 90vw; 14 | max-width: 542px; 15 | max-height: 85vh; 16 | 17 | background-color: var(--color-dark-700); 18 | border: 1px solid var(--color-dark-500, #333); 19 | border-radius: 2px; 20 | } 21 | 22 | .header { 23 | display: flex; 24 | align-items: center; 25 | justify-content: space-between; 26 | 27 | padding: 18px 24px; 28 | 29 | border-bottom: 1px solid var(--color-dark-500, #333); 30 | } 31 | 32 | .closeButton { 33 | all: unset; 34 | 35 | cursor: pointer; 36 | 37 | display: flex; 38 | 39 | width: 24px; 40 | height: 24px; 41 | } 42 | -------------------------------------------------------------------------------- /src/components/ui/radio-group/radio-group.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { RadioGroup } from './' 4 | 5 | const meta = { 6 | component: RadioGroup, 7 | tags: ['autodocs'], 8 | title: 'Components/Radio Group', 9 | } satisfies Meta 10 | 11 | export default meta 12 | type Story = StoryObj 13 | 14 | export const Default: Story = { 15 | args: { 16 | options: [ 17 | { label: 'Option One', value: 'option-one' }, 18 | { label: 'Option Two', value: 'option-two' }, 19 | ], 20 | }, 21 | } 22 | 23 | export const Disabled: Story = { 24 | args: { 25 | disabled: true, 26 | options: [ 27 | { label: 'Option One', value: 'option-one' }, 28 | { label: 'Option Two', value: 'option-two' }, 29 | ], 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /src/components/ui/text-field/text-field.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { TextField } from './' 4 | 5 | const meta = { 6 | component: TextField, 7 | tags: ['autodocs'], 8 | title: 'Components/TextField', 9 | } satisfies Meta 10 | 11 | export default meta 12 | type Story = StoryObj 13 | 14 | export const Default: Story = { 15 | args: { 16 | label: 'Label', 17 | placeholder: 'Placeholder', 18 | }, 19 | } 20 | 21 | export const Password: Story = { 22 | args: { 23 | label: 'Label', 24 | placeholder: 'Password', 25 | type: 'password', 26 | }, 27 | } 28 | 29 | export const Error: Story = { 30 | args: { 31 | errorMessage: 'Error message', 32 | label: 'Input with error', 33 | value: 'Wrong value', 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": false, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | 23 | /* Paths */ 24 | "types": ["node"], 25 | "paths": { 26 | "@/*": ["./src/*"] 27 | } 28 | }, 29 | "include": ["src"], 30 | "references": [{ "path": "./tsconfig.node.json" }] 31 | } 32 | -------------------------------------------------------------------------------- /src/components/ui/controlled/controlled-text-field/controlled-text-field.tsx: -------------------------------------------------------------------------------- 1 | import { Control, FieldPath, FieldValues, useController } from 'react-hook-form' 2 | 3 | import { TextField, TextFieldProps } from '@/components' 4 | 5 | export type ControlledTextFieldProps = { 6 | control: Control 7 | name: FieldPath 8 | } & Omit 9 | 10 | export const ControlledTextField = ( 11 | props: ControlledTextFieldProps 12 | ) => { 13 | const { 14 | field, 15 | fieldState: { error }, 16 | } = useController({ 17 | control: props.control, 18 | name: props.name, 19 | }) 20 | 21 | return 22 | } 23 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/menu-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/components/profile/personal-information/personal-information.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { PersonalInformation } from './' 4 | 5 | const meta = { 6 | component: PersonalInformation, 7 | tags: ['autodocs'], 8 | title: 'Profile/Personal information', 9 | } satisfies Meta 10 | 11 | export default meta 12 | type Story = StoryObj 13 | 14 | export const Default: Story = { 15 | args: { 16 | avatar: 'https://picsum.photos/200', 17 | email: 'your_email@domain.com', 18 | name: 'John Doe', 19 | onAvatarChange: () => { 20 | console.info('avatar changed') 21 | }, 22 | onLogout: () => { 23 | console.info('logout') 24 | }, 25 | onNameChange: () => { 26 | console.info('name changed') 27 | }, 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/more-vertical-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/ui/checkbox/checkbox.stories.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { Meta, StoryObj } from '@storybook/react' 4 | 5 | import { Checkbox } from './checkbox' 6 | const meta = { 7 | component: Checkbox, 8 | tags: ['autodocs'], 9 | title: 'Components/Checkbox', 10 | } satisfies Meta 11 | 12 | export default meta 13 | 14 | type Story = StoryObj 15 | export const Uncontrolled: Story = { 16 | args: { 17 | disabled: false, 18 | label: 'Click here', 19 | }, 20 | } 21 | 22 | export const Controlled: Story = { 23 | render: args => { 24 | const [checked, setChecked] = useState(false) 25 | 26 | return ( 27 | setChecked(!checked)} 32 | /> 33 | ) 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/bookmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/components-manual/keyboard-arrow-left.tsx: -------------------------------------------------------------------------------- 1 | import { Ref, SVGProps, forwardRef, memo } from 'react' 2 | const SvgComponent = (props: SVGProps, ref: Ref) => ( 3 | 12 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | const ForwardRef = forwardRef(SvgComponent) 26 | 27 | export default memo(ForwardRef) 28 | -------------------------------------------------------------------------------- /src/assets/icons/components-manual/keyboard-arrow-right.tsx: -------------------------------------------------------------------------------- 1 | import { Ref, SVGProps, forwardRef, memo } from 'react' 2 | const SvgComponent = (props: SVGProps, ref: Ref) => ( 3 | 12 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | const ForwardRef = forwardRef(SvgComponent) 26 | 27 | export default memo(ForwardRef) 28 | -------------------------------------------------------------------------------- /src/components/ui/modal/modal.stories.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { Modal } from '@/components' 4 | import { Meta, StoryObj } from '@storybook/react' 5 | 6 | const meta = { 7 | component: Modal, 8 | tags: ['autodocs'], 9 | title: 'Components/Modal', 10 | } satisfies Meta 11 | 12 | export default meta 13 | type Story = StoryObj 14 | 15 | export const Default: Story = { 16 | args: { 17 | children: 'Modal', 18 | onOpenChange: () => {}, 19 | open: true, 20 | title: 'Modal', 21 | }, 22 | render: args => { 23 | const [open, setOpen] = useState(false) 24 | 25 | return ( 26 | <> 27 | setOpen(true)}>Open Modal 28 | 29 | Modal content here 30 | 31 | > 32 | ) 33 | }, 34 | } 35 | -------------------------------------------------------------------------------- /src/assets/icons/components/Person.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgPerson = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | const ForwardRef = forwardRef(SvgPerson) 26 | const Memo = memo(ForwardRef) 27 | 28 | export default Memo 29 | -------------------------------------------------------------------------------- /src/components/layout/header/header.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Header } from './' 4 | 5 | const meta = { 6 | argTypes: { 7 | onLogout: { action: 'logout' }, 8 | }, 9 | component: Header, 10 | parameters: { 11 | layout: 'fullscreen', 12 | }, 13 | tags: ['autodocs'], 14 | title: 'Components/Header', 15 | } satisfies Meta 16 | 17 | export default meta 18 | type Story = StoryObj 19 | 20 | export const LoggedIn: Story = { 21 | // @ts-expect-error onLogout is required but it is provided through argTypes 22 | args: { 23 | avatar: 'https://avatars.githubusercontent.com/u/1196870?v=4', 24 | email: 'johndoe@gmail.com', 25 | isLoggedIn: true, 26 | userName: 'John Doe', 27 | }, 28 | } 29 | 30 | export const LoggedOut: Story = { 31 | args: { 32 | isLoggedIn: false, 33 | }, 34 | } 35 | -------------------------------------------------------------------------------- /src/components/ui/dialog/dialog.stories.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { Meta, StoryObj } from '@storybook/react' 4 | 5 | import { Dialog } from './' 6 | 7 | const meta = { 8 | component: Dialog, 9 | tags: ['autodocs'], 10 | title: 'Components/Dialog', 11 | } satisfies Meta 12 | 13 | export default meta 14 | type Story = StoryObj 15 | 16 | export const Default: Story = { 17 | args: { 18 | children: 'Modal', 19 | onOpenChange: () => {}, 20 | open: true, 21 | title: 'Modal', 22 | }, 23 | render: args => { 24 | const [open, setOpen] = useState(false) 25 | 26 | return ( 27 | <> 28 | setOpen(true)}>Open Modal 29 | 30 | Dialog content here 31 | 32 | > 33 | ) 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/arrow-ios-back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/email-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/arrow-ios-back-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/auth/check-email/check-email.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom' 2 | 3 | import s from './check-email.module.scss' 4 | 5 | import { Email } from '../../../assets/icons' 6 | import { Button, Card, Typography } from '../../ui' 7 | 8 | type Props = { 9 | email: string 10 | } 11 | 12 | export const CheckEmail = ({ email }: Props) => { 13 | const message = `We've sent an e-mail with instructions to ${email}` 14 | 15 | return ( 16 | 17 | 18 | Check your email 19 | 20 | 21 | 22 | 23 | 24 | {message} 25 | 26 | 27 | Back to Sign in 28 | 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/Email.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgEmail = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgEmail) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/components/ui/controlled/controlled-radio-group/controlled-radio-group.tsx: -------------------------------------------------------------------------------- 1 | import { Control, FieldPath, FieldValues, useController } from 'react-hook-form' 2 | 3 | import { RadioGroup, RadioGroupProps } from '@/components/ui' 4 | 5 | export type ControlledRadioGroupProps = { 6 | control: Control 7 | name: FieldPath 8 | } & Omit 9 | 10 | export const ControlledRadioGroup = ( 11 | props: ControlledRadioGroupProps 12 | ) => { 13 | const { 14 | field: { onChange, ...field }, 15 | fieldState: { error }, 16 | } = useController({ 17 | control: props.control, 18 | name: props.name, 19 | }) 20 | 21 | return ( 22 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/assets/icons/components/Block.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgBlock = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | const ForwardRef = forwardRef(SvgBlock) 28 | const Memo = memo(ForwardRef) 29 | 30 | export default Memo 31 | -------------------------------------------------------------------------------- /src/services/store.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' 2 | 3 | import { decksSlice } from '@/services/decks/decks.slice' 4 | import { configureStore } from '@reduxjs/toolkit' 5 | import { setupListeners } from '@reduxjs/toolkit/query/react' 6 | 7 | import { baseApi } from './base-api' 8 | 9 | export const store = configureStore({ 10 | middleware: getDefaultMiddleware => getDefaultMiddleware().concat(baseApi.middleware), 11 | reducer: { 12 | [baseApi.reducerPath]: baseApi.reducer, 13 | [decksSlice.name]: decksSlice.reducer, 14 | }, 15 | }) 16 | 17 | export type AppDispatch = typeof store.dispatch 18 | export type RootState = ReturnType 19 | 20 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 21 | export const useAppDispatch: () => AppDispatch = useDispatch 22 | export const useAppSelector: TypedUseSelectorHook = useSelector 23 | 24 | setupListeners(store.dispatch) 25 | -------------------------------------------------------------------------------- /src/assets/icons/components/MoreHorizontal.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgMoreHorizontal = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | const ForwardRef = forwardRef(SvgMoreHorizontal) 28 | const Memo = memo(ForwardRef) 29 | 30 | export default Memo 31 | -------------------------------------------------------------------------------- /src/components/layout/layout.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { LayoutPrimitive } from './' 4 | 5 | const meta = { 6 | argTypes: { 7 | onLogout: { action: 'logout' }, 8 | }, 9 | component: LayoutPrimitive, 10 | parameters: { 11 | layout: 'fullscreen', 12 | }, 13 | tags: ['autodocs'], 14 | title: 'Components/Layout', 15 | } satisfies Meta 16 | 17 | export default meta 18 | type Story = StoryObj 19 | 20 | export const LoggedIn: Story = { 21 | // @ts-expect-error onLogout is required but it is provided through argTypes 22 | args: { 23 | avatar: 'https://avatars.githubusercontent.com/u/1196870?v=4', 24 | children: 'Hello World', 25 | email: 'johndoe@gmail.com', 26 | isLoggedIn: true, 27 | userName: 'John Doe', 28 | }, 29 | } 30 | 31 | export const LoggedOut: Story = { 32 | args: { 33 | children: 'Hello World', 34 | isLoggedIn: false, 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /src/components/ui/spinner/spinner.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react' 2 | 3 | import { clsx } from 'clsx' 4 | 5 | import s from './spinner.module.scss' 6 | 7 | export type SpinnerProps = { 8 | fullScreen?: boolean 9 | size?: CSSProperties['width'] 10 | } & ComponentPropsWithoutRef<'span'> 11 | 12 | export const Spinner = forwardRef, SpinnerProps>( 13 | ({ className, fullScreen, size = '48px', style, ...rest }, ref) => { 14 | const styles = { 15 | height: size, 16 | width: size, 17 | ...style, 18 | } satisfies CSSProperties 19 | 20 | if (fullScreen) { 21 | return ( 22 | 23 | 24 | 25 | ) 26 | } 27 | 28 | return 29 | } 30 | ) 31 | -------------------------------------------------------------------------------- /src/components/ui/typography/typography.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithoutRef, ElementType, ReactNode } from 'react' 2 | 3 | import { clsx } from 'clsx' 4 | 5 | import s from './typography.module.scss' 6 | 7 | export interface TextProps { 8 | as?: T 9 | children?: ReactNode 10 | className?: string 11 | variant?: 12 | | 'body1' 13 | | 'body2' 14 | | 'caption' 15 | | 'error' 16 | | 'h1' 17 | | 'h2' 18 | | 'h3' 19 | | 'large' 20 | | 'link1' 21 | | 'link2' 22 | | 'overline' 23 | | 'subtitle1' 24 | | 'subtitle2' 25 | } 26 | 27 | export function Typography({ 28 | as, 29 | className, 30 | variant = 'body1', 31 | ...restProps 32 | }: Omit, keyof TextProps> & TextProps) { 33 | const classNames = clsx(s.text, s[variant], className) 34 | const Component = as || 'p' 35 | 36 | return 37 | } 38 | -------------------------------------------------------------------------------- /src/assets/icons/components/PauseCircle.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgPauseCircle = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgPauseCircle) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/PersonRemove.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgPersonRemove = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | const ForwardRef = forwardRef(SvgPersonRemove) 28 | const Memo = memo(ForwardRef) 29 | 30 | export default Memo 31 | -------------------------------------------------------------------------------- /src/assets/icons/components/PlusCircle.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgPlusCircle = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgPlusCircle) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/ArrowIosBack.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgArrowIosBack = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgArrowIosBack) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/Bookmark.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgBookmark = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgBookmark) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/Heart.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgHeart = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgHeart) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/ArrowIosUp.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgArrowIosUp = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgArrowIosUp) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/MoreHorizontalOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgMoreHorizontalOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | const ForwardRef = forwardRef(SvgMoreHorizontalOutline) 28 | const Memo = memo(ForwardRef) 29 | 30 | export default Memo 31 | -------------------------------------------------------------------------------- /src/assets/icons/components/PersonAdd.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgPersonAdd = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | const ForwardRef = forwardRef(SvgPersonAdd) 28 | const Memo = memo(ForwardRef) 29 | 30 | export default Memo 31 | -------------------------------------------------------------------------------- /src/assets/icons/components/PersonOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgPersonOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | const ForwardRef = forwardRef(SvgPersonOutline) 28 | const Memo = memo(ForwardRef) 29 | 30 | export default Memo 31 | -------------------------------------------------------------------------------- /src/components/ui/controlled/controlled-checkbox/controlled-checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { FieldValues, UseControllerProps, useController } from 'react-hook-form' 2 | 3 | import { Checkbox, CheckboxProps } from '../../' 4 | 5 | export type ControlledCheckboxProps = Omit< 6 | CheckboxProps, 7 | 'id' | 'onChange' | 'value' 8 | > & 9 | UseControllerProps 10 | 11 | export const ControlledCheckbox = ({ 12 | control, 13 | defaultValue, 14 | name, 15 | rules, 16 | shouldUnregister, 17 | ...checkboxProps 18 | }: ControlledCheckboxProps) => { 19 | const { 20 | field: { onChange, value }, 21 | } = useController({ 22 | control, 23 | defaultValue, 24 | name, 25 | rules, 26 | shouldUnregister, 27 | }) 28 | 29 | return ( 30 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /src/assets/icons/components/PlusSquare.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgPlusSquare = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgPlusSquare) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/ArrowIosForward.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgArrowIosForward = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgArrowIosForward) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/Search.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgSearch = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgSearch) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/LogOut.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgLogOut = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | const ForwardRef = forwardRef(SvgLogOut) 28 | const Memo = memo(ForwardRef) 29 | 30 | export default Memo 31 | -------------------------------------------------------------------------------- /src/assets/icons/components/Trash.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgTrash = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgTrash) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/Pin.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgPin = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgPin) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/ArrowIosBackOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgArrowIosBackOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgArrowIosBackOutline) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/components/Edit2.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgEdit2 = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | const ForwardRef = forwardRef(SvgEdit2) 28 | const Memo = memo(ForwardRef) 29 | 30 | export default Memo 31 | -------------------------------------------------------------------------------- /src/assets/icons/components/Home.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgHome = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgHome) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/PlusCircleOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgPlusCircleOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ) 26 | const ForwardRef = forwardRef(SvgPlusCircleOutline) 27 | const Memo = memo(ForwardRef) 28 | 29 | export default Memo 30 | -------------------------------------------------------------------------------- /src/assets/icons/components/RadioButtonUnchecked.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgRadioButtonUnchecked = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgRadioButtonUnchecked) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/icons/components/Image.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgImage = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgImage) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/ArrowIosDownOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgArrowIosDownOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgArrowIosDownOutline) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/CreditCard.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgCreditCard = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgCreditCard) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/EmailOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgEmailOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgEmailOutline) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/components/auth/sign-in/sign-in.module.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | width: 100%; 3 | max-width: 413px; 4 | padding: 33px 36px 29px; 5 | } 6 | 7 | .form { 8 | display: flex; 9 | flex-direction: column; 10 | gap: 24px; 11 | 12 | width: 100%; 13 | margin-bottom: 12px; 14 | } 15 | 16 | .checkbox { 17 | margin-bottom: 6px; 18 | } 19 | 20 | .title { 21 | margin-bottom: 27px; 22 | text-align: center; 23 | } 24 | 25 | .recoverPasswordLink, 26 | .recoverPasswordLink:visited { 27 | display: flex; 28 | justify-content: flex-end; 29 | 30 | width: 100%; 31 | margin-bottom: 66px; 32 | 33 | color: inherit; 34 | text-align: right; 35 | text-decoration: none; 36 | } 37 | 38 | .button { 39 | margin-bottom: 20px; 40 | } 41 | 42 | .caption { 43 | margin-bottom: 11px; 44 | color: var(--color-light-900); 45 | text-align: center; 46 | } 47 | 48 | .signUpLink { 49 | display: flex; 50 | justify-content: center; 51 | 52 | font-size: var(--font-size-m); 53 | font-weight: var(--font-weight-bold); 54 | color: var(--color-accent-500); 55 | } 56 | -------------------------------------------------------------------------------- /src/assets/icons/components/ArrowIosForwardOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgArrowIosForwardOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgArrowIosForwardOutline) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/SearchOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgSearchOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgSearchOutline) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/Close.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgClose = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgClose) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/LogOutOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgLogOutOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | const ForwardRef = forwardRef(SvgLogOutOutline) 28 | const Memo = memo(ForwardRef) 29 | 30 | export default Memo 31 | -------------------------------------------------------------------------------- /src/assets/icons/components/Expand.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgExpand = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | const ForwardRef = forwardRef(SvgExpand) 28 | const Memo = memo(ForwardRef) 29 | 30 | export default Memo 31 | -------------------------------------------------------------------------------- /src/assets/icons/components/Eye.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgEye = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgEye) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/Maximize.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgMaximize = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgMaximize) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/MoreVerticalOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgMoreVerticalOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgMoreVerticalOutline) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/Copy.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgCopy = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgCopy) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/PersonRemoveOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgPersonRemoveOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | const ForwardRef = forwardRef(SvgPersonRemoveOutline) 28 | const Memo = memo(ForwardRef) 29 | 30 | export default Memo 31 | -------------------------------------------------------------------------------- /src/assets/icons/components/Star.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgStar = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgStar) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/RadioButtonChecked.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgRadioButtonChecked = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgRadioButtonChecked) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/icons/components/HomeOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgHomeOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgHomeOutline) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/Mic.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgMic = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgMic) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/person.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/ui/table/table.module.scss: -------------------------------------------------------------------------------- 1 | .table { 2 | border-collapse: collapse; 3 | width: 100%; 4 | color: var(--color-light-100); 5 | border: 1px solid var(--color-dark-500); 6 | } 7 | 8 | .headCell { 9 | padding: 6px 24px; 10 | 11 | font-size: var(--font-size-s); 12 | font-weight: var(--font-weight-bold); 13 | line-height: var(--line-height-m); 14 | 15 | background-color: var(--color-dark-500); 16 | 17 | > span { 18 | user-select: none; 19 | display: flex; 20 | gap: 4px; 21 | align-items: center; 22 | } 23 | 24 | &.sortable > span { 25 | cursor: pointer; 26 | } 27 | 28 | svg { 29 | transition: transform 0.2s ease-in-out; 30 | } 31 | } 32 | 33 | .tableCell { 34 | padding: 6px 24px; 35 | 36 | font-size: var(--font-size-s); 37 | line-height: var(--line-height-m); 38 | 39 | background-color: var(--color-bg-table); 40 | border-bottom: 1px solid var(--color-dark-500); 41 | } 42 | 43 | .chevronDown { 44 | transform: rotate(180deg); 45 | } 46 | 47 | .empty { 48 | display: flex; 49 | justify-content: center; 50 | width: 100%; 51 | } 52 | -------------------------------------------------------------------------------- /src/assets/icons/components/CloseOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgCloseOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgCloseOutline) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/PauseCircleOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgPauseCircleOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgPauseCircleOutline) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/ExpandOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgExpandOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | const ForwardRef = forwardRef(SvgExpandOutline) 28 | const Memo = memo(ForwardRef) 29 | 30 | export default Memo 31 | -------------------------------------------------------------------------------- /src/assets/icons/components/PersonAddOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgPersonAddOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | const ForwardRef = forwardRef(SvgPersonAddOutline) 28 | const Memo = memo(ForwardRef) 29 | 30 | export default Memo 31 | -------------------------------------------------------------------------------- /src/assets/icons/components/TrashOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgTrashOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgTrashOutline) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/components/decks/delete-deck-dialog/delete-deck-dialog.stories.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { Button } from '@/components' 4 | import { Meta, StoryObj } from '@storybook/react' 5 | 6 | import { DeleteDeckDialog } from './' 7 | 8 | const meta = { 9 | component: DeleteDeckDialog, 10 | tags: ['autodocs'], 11 | title: 'Decks/Delete Deck Dialog', 12 | } satisfies Meta 13 | 14 | export default meta 15 | type Story = StoryObj 16 | 17 | export const Default: Story = { 18 | args: { 19 | deckName: 'Deck Name', 20 | onOpenChange: () => {}, 21 | open: true, 22 | }, 23 | render: args => { 24 | const [open, setOpen] = useState(false) 25 | const closeModal = () => setOpen(false) 26 | 27 | return ( 28 | <> 29 | setOpen(true)}>Open Modal 30 | 37 | > 38 | ) 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /src/assets/icons/components/ArrowBackOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgArrowBackOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgArrowBackOutline) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/Calendar.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgCalendar = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgCalendar) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/FunnelOutline1.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgFunnelOutline1 = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgFunnelOutline1) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/PaperPlane.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgPaperPlane = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgPaperPlane) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/PlayCircle.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgPlayCircle = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgPlayCircle) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/home-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/components/Edit2Outline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgEdit2Outline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | const ForwardRef = forwardRef(SvgEdit2Outline) 28 | const Memo = memo(ForwardRef) 29 | 30 | export default Memo 31 | -------------------------------------------------------------------------------- /src/components/layout/header/header.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react' 2 | import { Link } from 'react-router-dom' 3 | 4 | import { Logo } from '@/assets' 5 | import { UserDropdown, UserDropdownProps } from '@/components/layout/header/user-dropdown' 6 | 7 | import s from './header.module.scss' 8 | 9 | import { Button } from '../../ui' 10 | 11 | export type HeaderProps = 12 | | ({ 13 | isLoggedIn: false 14 | } & Partial) 15 | | ({ 16 | isLoggedIn: true 17 | } & UserDropdownProps) 18 | 19 | export const Header = memo(({ avatar, email, isLoggedIn, onLogout, userName }: HeaderProps) => { 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | {isLoggedIn && ( 27 | 28 | )} 29 | {!isLoggedIn && ( 30 | 31 | Sign In 32 | 33 | )} 34 | 35 | 36 | ) 37 | }) 38 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/paper-plane.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/components/MessageCircle.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgMessageCircle = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgMessageCircle) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/funnel-outline 1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/paper-plane-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/components/ArrowForwardOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgArrowForwardOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgArrowForwardOutline) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/arrow-ios-forward.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/components/CreditCardOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgCreditCardOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgCreditCardOutline) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/ImageOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgImageOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgImageOutline) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/arrow-ios-forward-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/components/PaperPlaneOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgPaperPlaneOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgPaperPlaneOutline) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/TrendingUp.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgTrendingUp = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgTrendingUp) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/PinOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgPinOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ) 30 | const ForwardRef = forwardRef(SvgPinOutline) 31 | const Memo = memo(ForwardRef) 32 | 33 | export default Memo 34 | -------------------------------------------------------------------------------- /src/assets/icons/components/PlusSquareOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgPlusSquareOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ) 30 | const ForwardRef = forwardRef(SvgPlusSquareOutline) 31 | const Memo = memo(ForwardRef) 32 | 33 | export default Memo 34 | -------------------------------------------------------------------------------- /src/components/ui/spinner/spinner.module.scss: -------------------------------------------------------------------------------- 1 | .fullScreenContainer { 2 | position: fixed; 3 | z-index: 9999; 4 | top: var(--header-height); 5 | left: 0; 6 | 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | 11 | width: 100vw; 12 | height: calc(100vh - var(--header-height)); 13 | 14 | background-color: rgb(0 0 0 / 50%); 15 | } 16 | 17 | .loader { 18 | position: relative; 19 | 20 | display: inline-block; 21 | 22 | box-sizing: border-box; 23 | width: 48px; 24 | height: 48px; 25 | 26 | border: 3px solid #fff; 27 | border-radius: 50%; 28 | 29 | animation: rotation 1s linear infinite; 30 | } 31 | 32 | .loader::after { 33 | content: ''; 34 | 35 | position: absolute; 36 | top: 50%; 37 | left: 50%; 38 | transform: translate(-50%, -50%); 39 | 40 | box-sizing: border-box; 41 | width: 40px; 42 | height: 40px; 43 | 44 | border: 3px solid transparent; 45 | border-bottom-color: var(--color-accent-500); 46 | border-radius: 50%; 47 | } 48 | 49 | @keyframes rotation { 50 | 0% { 51 | transform: rotate(0deg); 52 | } 53 | 54 | 100% { 55 | transform: rotate(360deg); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/pages/deck-page/deck-page.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { Link, useParams } from 'react-router-dom' 3 | 4 | import { Button, CardsTable, TextField, Typography } from '@/components' 5 | import { Pagination } from '@/components/ui/pagination' 6 | import { useGetDeckByIdQuery, useGetDeckCardsQuery } from '@/services' 7 | 8 | export const DeckPage = () => { 9 | const { deckId } = useParams() 10 | const [currentPage, setCurrentPage] = useState(1) 11 | const { data: deckData } = useGetDeckByIdQuery({ id: deckId || '' }) 12 | const { data: cardsData } = useGetDeckCardsQuery({ id: deckId || '' }) 13 | 14 | const learnLink = `/decks/${deckId}/learn` 15 | 16 | return ( 17 | 18 | {deckData?.name} 19 | 20 | Learn 21 | 22 | 23 | 24 | 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/MenuOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgMenuOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | const ForwardRef = forwardRef(SvgMenuOutline) 28 | const Memo = memo(ForwardRef) 29 | 30 | export default Memo 31 | -------------------------------------------------------------------------------- /src/assets/icons/components/PlayCircleOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgPlayCircleOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgPlayCircleOutline) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/MaximizeOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgMaximizeOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ) 30 | const ForwardRef = forwardRef(SvgMaximizeOutline) 31 | const Memo = memo(ForwardRef) 32 | 33 | export default Memo 34 | -------------------------------------------------------------------------------- /src/assets/icons/components/TrendingUpOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgTrendingUpOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgTrendingUpOutline) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/hooks/use-query-param/use-query-param.ts: -------------------------------------------------------------------------------- 1 | import { isNil } from 'remeda' 2 | 3 | export function useQueryParam( 4 | searchParams: URLSearchParams, 5 | setSearchParams: (searchParams: URLSearchParams) => void, 6 | param: string, 7 | defaultValue?: T 8 | ): [T | null, (value: T | null) => void] { 9 | const paramValue = searchParams.get(param) 10 | const convertedValue = getConvertedValue(paramValue, defaultValue) 11 | 12 | const setParamValue = (value: T | null): void => { 13 | if (isNil(value) || value === '') { 14 | searchParams.delete(param) 15 | } else { 16 | searchParams.set(param, String(value)) 17 | } 18 | setSearchParams(searchParams) 19 | } 20 | 21 | return [convertedValue, setParamValue] 22 | } 23 | 24 | function getConvertedValue(value: null | string, defaultValue: T | undefined): T | null { 25 | if (value === null) { 26 | return defaultValue ?? null 27 | } 28 | if (value === 'true' || value === 'false') { 29 | return (value === 'true') as unknown as T 30 | } 31 | if (!isNaN(Number(value))) { 32 | return Number(value) as unknown as T 33 | } 34 | 35 | return value as unknown as T 36 | } 37 | -------------------------------------------------------------------------------- /src/assets/icons/components/BookmarkOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgBookmarkOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgBookmarkOutline) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/arrow-ios-Up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/trash-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/components/CalendarOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgCalendarOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgCalendarOutline) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/components/MicOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgMicOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ) 32 | const ForwardRef = forwardRef(SvgMicOutline) 33 | const Memo = memo(ForwardRef) 34 | 35 | export default Memo 36 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/arrow-ios-Down-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/components/HeartOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgHeartOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgHeartOutline) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/edit-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/icons/components/EyeOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgEyeOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ) 30 | const ForwardRef = forwardRef(SvgEyeOutline) 31 | const Memo = memo(ForwardRef) 32 | 33 | export default Memo 34 | -------------------------------------------------------------------------------- /src/components/decks/cards-table.tsx: -------------------------------------------------------------------------------- 1 | import { Column, Table, TableBody, TableCell, TableHeader, TableRow } from '@/components' 2 | import { Card } from '@/services/decks' 3 | import { formatDate } from '@/utils' 4 | 5 | const columns: Column[] = [ 6 | { 7 | key: 'question', 8 | sortable: true, 9 | title: 'Question', 10 | }, 11 | { 12 | key: 'answer', 13 | sortable: true, 14 | title: 'Answer', 15 | }, 16 | { 17 | key: 'updated', 18 | sortable: true, 19 | title: 'Last Updated', 20 | }, 21 | { 22 | key: 'grade', 23 | sortable: true, 24 | title: 'Grade', 25 | }, 26 | ] 27 | 28 | type Props = { 29 | cards: Card[] | undefined 30 | } 31 | export const CardsTable = ({ cards }: Props) => { 32 | return ( 33 | 34 | 35 | 36 | {cards?.map(card => ( 37 | 38 | {card.question} 39 | {card.answer} 40 | {formatDate(card.updated)} 41 | {card.grade} 42 | 43 | ))} 44 | 45 | 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/pin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/image-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/icons/components-manual/visibility-off.tsx: -------------------------------------------------------------------------------- 1 | import { Ref, SVGProps, forwardRef, memo } from 'react' 2 | const SvgComponent = (props: SVGProps, ref: Ref) => ( 3 | 12 | 18 | 19 | ) 20 | const ForwardRef = forwardRef(SvgComponent) 21 | 22 | export default memo(ForwardRef) 23 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/arrow-back-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/eye.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/plus-square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/components/CopyOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgCopyOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ) 32 | const ForwardRef = forwardRef(SvgCopyOutline) 33 | const Memo = memo(ForwardRef) 34 | 35 | export default Memo 36 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/bookmark-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/ui/button/button.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ComponentPropsWithoutRef, 3 | ElementRef, 4 | ElementType, 5 | ForwardedRef, 6 | ReactNode, 7 | forwardRef, 8 | } from 'react' 9 | 10 | import { clsx } from 'clsx' 11 | 12 | import s from './button.module.scss' 13 | 14 | export type ButtonProps = { 15 | as?: T 16 | children: ReactNode 17 | className?: string 18 | fullWidth?: boolean 19 | variant?: 'icon' | 'link' | 'primary' | 'secondary' | 'tertiary' 20 | } & ComponentPropsWithoutRef 21 | 22 | const ButtonPolymorph = (props: ButtonProps, ref: any) => { 23 | const { 24 | as: Component = 'button', 25 | className, 26 | fullWidth, 27 | rounded, 28 | variant = 'primary', 29 | ...rest 30 | } = props 31 | 32 | return ( 33 | 38 | ) 39 | } 40 | 41 | export const Button = forwardRef(ButtonPolymorph) as ( 42 | props: { 43 | ref?: ForwardedRef> 44 | } & ButtonProps & 45 | Omit, keyof ButtonProps> 46 | ) => ReturnType 47 | -------------------------------------------------------------------------------- /src/router.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Navigate, 3 | Outlet, 4 | RouteObject, 5 | RouterProvider, 6 | createBrowserRouter, 7 | } from 'react-router-dom' 8 | 9 | import { Layout, useAuthContext } from '@/components/layout' 10 | import { DeckPage } from '@/pages/deck-page/deck-page' 11 | 12 | import { DecksPage, SignInPage } from './pages' 13 | 14 | const publicRoutes: RouteObject[] = [ 15 | { 16 | children: [ 17 | { 18 | element: , 19 | path: '/login', 20 | }, 21 | ], 22 | element: , 23 | }, 24 | ] 25 | 26 | const privateRoutes: RouteObject[] = [ 27 | { 28 | element: , 29 | path: '/', 30 | }, 31 | { 32 | element: , 33 | path: '/decks/:deckId', 34 | }, 35 | ] 36 | 37 | const router = createBrowserRouter([ 38 | { 39 | children: [ 40 | { 41 | children: privateRoutes, 42 | element: , 43 | }, 44 | ...publicRoutes, 45 | ], 46 | element: , 47 | }, 48 | ]) 49 | 50 | export const Router = () => { 51 | return 52 | } 53 | 54 | function PrivateRoutes() { 55 | const { isAuthenticated } = useAuthContext() 56 | 57 | return isAuthenticated ? : 58 | } 59 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/edit-2-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/icons/components/StarOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgStarOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | const ForwardRef = forwardRef(SvgStarOutline) 29 | const Memo = memo(ForwardRef) 30 | 31 | export default Memo 32 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/arrow-forward-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/ui/pagination/pagination.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | gap: 25px; 4 | align-items: center; 5 | } 6 | 7 | .container { 8 | display: flex; 9 | gap: 12px; 10 | list-style-type: none; 11 | } 12 | 13 | @mixin item { 14 | all: unset; 15 | 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | 20 | width: 24px; 21 | height: 24px; 22 | 23 | color: var(--color-light-100); 24 | 25 | border-radius: 2px; 26 | } 27 | 28 | .item { 29 | @include item; 30 | 31 | cursor: pointer; 32 | 33 | &:focus-visible { 34 | outline: var(--outline-focus); 35 | } 36 | 37 | &:disabled { 38 | cursor: initial; 39 | opacity: 1; 40 | } 41 | 42 | &:hover:not(&:disabled) { 43 | background-color: var(--color-dark-500); 44 | } 45 | 46 | &.selected { 47 | color: var(--color-dark-900); 48 | background-color: var(--color-light-100); 49 | } 50 | } 51 | 52 | .dots { 53 | @include item; 54 | } 55 | 56 | .icon { 57 | .item:disabled & { 58 | // important because icons have style prop 59 | color: var(--color-action-disabled) !important; 60 | } 61 | } 62 | 63 | .selectBox { 64 | display: flex; 65 | gap: 12px; 66 | align-items: center; 67 | 68 | color: var(--color-light-100); 69 | white-space: nowrap; 70 | } 71 | 72 | .select { 73 | flex-shrink: 0; 74 | width: 50px; 75 | } 76 | -------------------------------------------------------------------------------- /src/styles/_colors.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | // accent 3 | --color-accent-100: #bea6ff; 4 | --color-accent-300: #a280ff; 5 | --color-accent-500: #8c61ff; 6 | --color-accent-700: #704ecc; 7 | --color-accent-900: #382766; 8 | 9 | // success 10 | --color-success-100: #80ffbf; 11 | --color-success-300: #22e584; 12 | --color-success-500: #14cc70; 13 | --color-success-700: #0f9954; 14 | --color-success-900: #0a6638; 15 | 16 | // danger 17 | --color-danger-100: #ff8099; 18 | --color-danger-300: #f23d61; 19 | --color-danger-500: #cc1439; 20 | --color-danger-700: #990f2b; 21 | --color-danger-900: #660a1d; 22 | 23 | // warning 24 | --color-warning-100: #ffd073; 25 | --color-warning-300: #e5ac39; 26 | --color-warning-500: #d99000; 27 | --color-warning-700: #960; 28 | --color-warning-900: #640; 29 | 30 | // info 31 | --color-info-100: #73a5ff; 32 | --color-info-300: #4c8dff; 33 | --color-info-500: #397df6; 34 | --color-info-700: #2f68cc; 35 | --color-info-900: #234e99; 36 | 37 | // light 38 | --color-light-100: #fff; 39 | --color-light-300: #f9f7ff; 40 | --color-light-500: #f4f2fa; 41 | --color-light-700: #dcdae0; 42 | --color-light-900: #c3c1c7; 43 | 44 | // dark 45 | --color-dark-100: #808080; 46 | --color-dark-300: #4c4c4c; 47 | --color-dark-500: #333; 48 | --color-dark-700: #171717; 49 | --color-dark-900: #000; 50 | } 51 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/Block.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/ui/modal/modal.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithoutRef, ReactNode } from 'react' 2 | 3 | import { Close } from '@/assets' 4 | import { Typography } from '@/components' 5 | import * as DialogPrimitive from '@radix-ui/react-dialog' 6 | 7 | import s from './modal.module.scss' 8 | 9 | export type ModalProps = { 10 | children: ReactNode 11 | onOpenChange: (open: boolean) => void 12 | open: boolean 13 | title?: string 14 | } & Omit, 'onOpenChange' | 'open'> 15 | export const Modal = ({ children, title, ...props }: ModalProps) => { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {title} 25 | 26 | 27 | 28 | 29 | 30 | 31 | {children} 32 | 33 | 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /src/assets/icons/components/MessageCircleOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | import { Ref, forwardRef, memo } from 'react' 3 | const SvgMessageCircleOutline = (props: SVGProps, ref: Ref) => ( 4 | 13 | 14 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ) 32 | const ForwardRef = forwardRef(SvgMessageCircleOutline) 33 | const Memo = memo(ForwardRef) 34 | 35 | export default Memo 36 | -------------------------------------------------------------------------------- /src/components/layout/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import { Outlet, useOutletContext } from 'react-router-dom' 3 | 4 | import { Header, HeaderProps, Spinner } from '@/components' 5 | import { useMeQuery } from '@/services/auth/auth.service' 6 | 7 | import s from './layout.module.scss' 8 | 9 | type AuthContext = { 10 | isAuthenticated: boolean 11 | } 12 | 13 | export function useAuthContext() { 14 | return useOutletContext() 15 | } 16 | 17 | export const Layout = () => { 18 | const { data, isError, isLoading } = useMeQuery() 19 | const isAuthenticated = !isError && !isLoading 20 | 21 | if (isLoading) { 22 | return 23 | } 24 | 25 | return ( 26 | {}} 31 | userName={data?.name ?? ''} 32 | > 33 | 34 | 35 | ) 36 | } 37 | 38 | export type LayoutPrimitiveProps = { children: ReactNode } & HeaderProps 39 | 40 | export const LayoutPrimitive = ({ children, ...headerProps }: LayoutPrimitiveProps) => { 41 | return ( 42 | 43 | 44 | {children} 45 | 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /src/components/ui/tabs/tabs.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react' 2 | 3 | import * as TabsPrimitive from '@radix-ui/react-tabs' 4 | import { clsx } from 'clsx' 5 | 6 | import s from './tabs.module.scss' 7 | 8 | const Tabs = TabsPrimitive.Root 9 | 10 | const TabsList = forwardRef< 11 | ElementRef, 12 | ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 15 | )) 16 | 17 | TabsList.displayName = TabsPrimitive.List.displayName 18 | 19 | const TabsTrigger = forwardRef< 20 | ElementRef, 21 | ComponentPropsWithoutRef 22 | >(({ className, ...props }, ref) => ( 23 | 24 | )) 25 | 26 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName 27 | 28 | const TabsContent = forwardRef< 29 | ElementRef, 30 | ComponentPropsWithoutRef 31 | >(({ className, ...props }, ref) => ( 32 |
13 | Do you really want to remove {deckName}? 14 |
All cards will be deleted.