├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── .vscode └── settings.json ├── README.md ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── _document.tsx └── index.tsx ├── public ├── favicon.ico ├── next.svg └── vercel.svg ├── screenshots ├── Screenshot1.png ├── Screenshot2.png └── Screenshot3.png ├── src ├── app │ ├── index.tsx │ ├── providers │ │ ├── cache │ │ │ └── index.tsx │ │ ├── dialogs │ │ │ └── index.tsx │ │ ├── index.tsx │ │ └── theme │ │ │ ├── config.ts │ │ │ └── index.tsx │ └── styles │ │ └── index.css ├── entities │ ├── column │ │ ├── index.ts │ │ ├── types.ts │ │ └── ui │ │ │ ├── index.ts │ │ │ └── item │ │ │ ├── constants.ts │ │ │ └── index.tsx │ ├── table │ │ ├── index.ts │ │ ├── lib.ts │ │ ├── model.ts │ │ └── types.ts │ ├── task │ │ ├── index.ts │ │ ├── lib │ │ │ └── index.ts │ │ ├── model.ts │ │ ├── types.ts │ │ └── ui │ │ │ ├── card │ │ │ ├── index.tsx │ │ │ └── lib.ts │ │ │ └── index.ts │ └── todo │ │ ├── index.ts │ │ ├── item │ │ ├── index.ts │ │ └── ui │ │ │ └── index.tsx │ │ └── types.ts ├── features │ ├── column │ │ ├── drag-and-drop │ │ │ ├── constants.ts │ │ │ ├── index.ts │ │ │ ├── model │ │ │ │ ├── index.ts │ │ │ │ ├── model.ts │ │ │ │ └── use-column-drag-and-drop.ts │ │ │ ├── types.ts │ │ │ └── ui │ │ │ │ └── index.tsx │ │ ├── index.ts │ │ ├── select │ │ │ ├── index.ts │ │ │ ├── model │ │ │ │ ├── index.ts │ │ │ │ ├── model.ts │ │ │ │ └── use-coloumn-select.ts │ │ │ ├── types.ts │ │ │ └── ui │ │ │ │ └── index.tsx │ │ └── switcher │ │ │ ├── index.ts │ │ │ ├── model │ │ │ ├── index.ts │ │ │ └── model.ts │ │ │ └── ui │ │ │ └── index.tsx │ ├── table │ │ ├── create │ │ │ ├── index.ts │ │ │ ├── model │ │ │ │ ├── index.ts │ │ │ │ ├── model.ts │ │ │ │ ├── schema.ts │ │ │ │ └── use-table-form.ts │ │ │ ├── types.ts │ │ │ └── ui │ │ │ │ ├── column-field │ │ │ │ └── index.tsx │ │ │ │ ├── column-form │ │ │ │ └── index.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── title-field │ │ │ │ └── index.tsx │ │ ├── delete │ │ │ ├── index.ts │ │ │ ├── model │ │ │ │ ├── index.ts │ │ │ │ ├── model.ts │ │ │ │ └── use-delete-table.ts │ │ │ └── ui │ │ │ │ └── index.tsx │ │ ├── index.ts │ │ ├── set-table-index │ │ │ └── index.ts │ │ └── update │ │ │ ├── index.ts │ │ │ ├── model │ │ │ ├── index.ts │ │ │ ├── model.ts │ │ │ ├── schema.ts │ │ │ └── use-table-form.ts │ │ │ ├── types.ts │ │ │ └── ui │ │ │ ├── column-field │ │ │ └── index.tsx │ │ │ ├── column-form │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ └── title-field │ │ │ └── index.tsx │ ├── task │ │ ├── create │ │ │ ├── index.ts │ │ │ ├── model │ │ │ │ ├── index.ts │ │ │ │ ├── model.ts │ │ │ │ ├── schema.ts │ │ │ │ └── use-task-form.ts │ │ │ ├── types.ts │ │ │ └── ui │ │ │ │ ├── column-select │ │ │ │ └── index.tsx │ │ │ │ ├── description-field │ │ │ │ └── index.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── title-field │ │ │ │ └── index.tsx │ │ │ │ ├── todo-field │ │ │ │ └── index.tsx │ │ │ │ └── todo-form │ │ │ │ └── index.tsx │ │ ├── delete │ │ │ ├── index.ts │ │ │ ├── model │ │ │ │ ├── index.ts │ │ │ │ ├── model.ts │ │ │ │ ├── use-delete-dialog.ts │ │ │ │ └── use-task-delete.ts │ │ │ ├── types.ts │ │ │ └── ui │ │ │ │ └── index.tsx │ │ ├── index.ts │ │ └── update │ │ │ ├── index.ts │ │ │ ├── model │ │ │ ├── index.ts │ │ │ ├── model.ts │ │ │ ├── schema.ts │ │ │ ├── use-task-form.ts │ │ │ └── use-update-dialog.ts │ │ │ ├── types.ts │ │ │ └── ui │ │ │ ├── column-select │ │ │ └── index.tsx │ │ │ ├── description-field │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ ├── title-field │ │ │ └── index.tsx │ │ │ ├── todo-field │ │ │ └── index.tsx │ │ │ └── todo-form │ │ │ └── index.tsx │ ├── theme │ │ ├── index.ts │ │ └── switcher │ │ │ ├── index.tsx │ │ │ └── model.ts │ └── todo │ │ ├── index.ts │ │ └── toggle │ │ ├── index.ts │ │ ├── model │ │ ├── index.ts │ │ ├── model.ts │ │ └── use-toggle-todo.ts │ │ ├── types.ts │ │ └── ui │ │ └── index.tsx ├── pages │ └── home │ │ ├── index.ts │ │ └── ui │ │ ├── index.tsx │ │ └── welcome-card │ │ ├── get-started-button │ │ ├── index.tsx │ │ └── model.ts │ │ └── index.tsx ├── shared │ ├── assets │ │ ├── data.json │ │ └── images │ │ │ ├── index.ts │ │ │ ├── logo.svg │ │ │ ├── task.png │ │ │ └── task.svg │ ├── lib │ │ ├── boolean │ │ │ └── index.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── use-event-listener │ │ │ │ └── index.ts │ │ │ ├── use-event │ │ │ │ └── index.ts │ │ │ ├── use-is-mobile │ │ │ │ └── index.ts │ │ │ ├── use-media-query │ │ │ │ └── index.ts │ │ │ └── use-toggle │ │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── modal │ │ │ └── index.ts │ │ └── nextjs │ │ │ ├── index.ts │ │ │ └── no-ssr │ │ │ └── index.tsx │ └── ui │ │ ├── drawer │ │ └── index.tsx │ │ ├── index.ts │ │ └── snackbar │ │ ├── index.ts │ │ ├── lib │ │ └── index.ts │ │ ├── model │ │ └── index.ts │ │ ├── types.ts │ │ └── ui │ │ ├── index.tsx │ │ └── item │ │ └── index.tsx └── widgets │ ├── column │ ├── index.ts │ └── list │ │ └── index.tsx │ ├── header │ ├── constants.ts │ ├── index.ts │ └── ui │ │ ├── actions │ │ ├── create-task-button │ │ │ └── index.tsx │ │ ├── index.tsx │ │ └── menu-button │ │ │ └── index.tsx │ │ ├── index.tsx │ │ └── title │ │ └── index.tsx │ ├── layout │ ├── constants.ts │ ├── index.ts │ ├── lib.ts │ ├── model.ts │ └── ui │ │ ├── index.tsx │ │ └── toggle-button │ │ └── index.tsx │ ├── logo │ └── index.tsx │ ├── mobile-menu │ └── index.tsx │ ├── sidebar │ ├── constants.ts │ ├── index.ts │ └── ui │ │ └── index.tsx │ ├── table │ ├── index.ts │ └── list │ │ ├── create-table-button │ │ └── index.tsx │ │ ├── index.tsx │ │ └── model.ts │ ├── task │ ├── detial │ │ ├── index.ts │ │ ├── model.ts │ │ └── ui │ │ │ ├── index.tsx │ │ │ └── menu-button │ │ │ └── index.tsx │ └── index.ts │ └── theme │ ├── index.ts │ └── ui.tsx └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["uuidv"] 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kanban task management app 2 | 3 | Streamline your tasks with our task manager. Stay organized, prioritize 4 | your to-do list, and track progress all in one place. Boost productivity 5 | and achieve your goals effortlessly. 6 | 7 | ### Screenshot 8 | 9 | ![Screenshot](./screenshots/Screenshot1.png) 10 | ![Screenshot](./screenshots/Screenshot2.png) 11 | ![Screenshot](./screenshots/Screenshot3.png) 12 | 13 | ### Used technologies 14 | 15 | - NextJS 16 | - Joy UI 17 | - Effector 18 | - Yup 19 | - React hook forms 20 | - FSD (Methodology) 21 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: process.env.NODE_ENV === 'development', 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-task-management-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "cross-env NODE_ENV=development next dev", 7 | "build": "next build", 8 | "start": "cross-env NODE_ENV=production next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@emotion/react": "^11.11.4", 13 | "@emotion/styled": "^11.11.0", 14 | "@fontsource/inter": "^5.0.17", 15 | "@hookform/resolvers": "^3.4.2", 16 | "@iconscout/react-unicons": "^2.0.2", 17 | "@mui/joy": "^5.0.0-beta.32", 18 | "@types/uuid": "^9.0.8", 19 | "effector": "^23.2.0", 20 | "effector-react": "^23.2.0", 21 | "effector-storage": "^7.1.0", 22 | "next": "14.1.4", 23 | "react": "^18", 24 | "react-dom": "^18", 25 | "react-hook-form": "^7.51.2", 26 | "uuid": "^9.0.1", 27 | "yup": "^1.4.0" 28 | }, 29 | "devDependencies": { 30 | "@trivago/prettier-plugin-sort-imports": "^4.3.0", 31 | "@types/node": "^20", 32 | "@types/react": "^18", 33 | "@types/react-dom": "^18", 34 | "cross-env": "^7.0.3", 35 | "eslint": "^8", 36 | "eslint-config-next": "14.1.4", 37 | "typescript": "^5" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | export { App as default } from 'app' 2 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | export { HomePage as default } from 'pages/home' 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Un1T3G/nextjs-task-management-app/7315f8bdcfe9a7047bfcafed83e81f326f7eab76/public/favicon.ico -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screenshots/Screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Un1T3G/nextjs-task-management-app/7315f8bdcfe9a7047bfcafed83e81f326f7eab76/screenshots/Screenshot1.png -------------------------------------------------------------------------------- /screenshots/Screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Un1T3G/nextjs-task-management-app/7315f8bdcfe9a7047bfcafed83e81f326f7eab76/screenshots/Screenshot2.png -------------------------------------------------------------------------------- /screenshots/Screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Un1T3G/nextjs-task-management-app/7315f8bdcfe9a7047bfcafed83e81f326f7eab76/screenshots/Screenshot3.png -------------------------------------------------------------------------------- /src/app/index.tsx: -------------------------------------------------------------------------------- 1 | import '@fontsource/inter' 2 | import { NoSSR } from 'shared/lib' 3 | import type { AppProps } from 'next/app' 4 | 5 | import { Providers } from './providers' 6 | import './styles/index.css' 7 | import { RootLayout } from 'widgets/layout' 8 | 9 | export const App = ({ Component, pageProps }: AppProps) => { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/app/providers/cache/index.tsx: -------------------------------------------------------------------------------- 1 | import createCache from '@emotion/cache' 2 | import { CacheProvider as _CacheProvider } from '@emotion/react' 3 | import { useServerInsertedHTML } from 'next/navigation' 4 | import { PropsWithChildren, useState } from 'react' 5 | 6 | export const CacheProvider = ({ 7 | pageProps, 8 | children, 9 | }: PropsWithChildren<{ pageProps: any }>) => { 10 | const [{ cache, flush }] = useState(() => { 11 | const cache = createCache({ 12 | key: 'uni', 13 | }) 14 | cache.compat = true 15 | const prevInsert = cache.insert 16 | let inserted: string[] = [] 17 | cache.insert = (...args) => { 18 | const serialized = args[1] 19 | if (cache.inserted[serialized.name] === undefined) { 20 | inserted.push(serialized.name) 21 | } 22 | return prevInsert(...args) 23 | } 24 | const flush = () => { 25 | const prevInserted = inserted 26 | inserted = [] 27 | return prevInserted 28 | } 29 | return { cache, flush } 30 | }) 31 | 32 | useServerInsertedHTML(() => { 33 | const names = flush() 34 | if (names.length === 0) { 35 | return null 36 | } 37 | let styles = '' 38 | for (const name of names) { 39 | styles += cache.inserted[name] 40 | } 41 | return ( 42 | 41 | 42 | 44 | 45 | 46 | 48 | 64 | 65 | 82 | 83 | 84 | 85 | 88 | 89 | 90 | 92 | 94 | 95 | 96 | 97 | 98 | 99 | 101 | 102 | 104 | 105 | 106 | 108 | 109 | 110 | 114 | 115 | 116 | 120 | 121 | 122 | 124 | 125 | 126 | 128 | 129 | 130 | 132 | 133 | 134 | 136 | 137 | 138 | 140 | 141 | 142 | 143 | 144 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 154 | 155 | 156 | 158 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 168 | 170 | 172 | 174 | 176 | 178 | 179 | 180 | 184 | 185 | 186 | 190 | 191 | 192 | 194 | 196 | 198 | 199 | 200 | 201 | 202 | 207 | 208 | 211 | 217 | 222 | 223 | 224 | 225 | 227 | 228 | 229 | 235 | 236 | 237 | 242 | 243 | 254 | 264 | 265 | 266 | 267 | 269 | 270 | 271 | 276 | 277 | 278 | 283 | 284 | 297 | 310 | 311 | 312 | 317 | 318 | 319 | 323 | 327 | 331 | 332 | 333 | 335 | 336 | 337 | 342 | 343 | 349 | 350 | 351 | 355 | 356 | 359 | 361 | 367 | 371 | 372 | 373 | 374 | 375 | -------------------------------------------------------------------------------- /src/shared/lib/boolean/index.ts: -------------------------------------------------------------------------------- 1 | export const bool2str = (bool: boolean): string => (bool ? 'true' : 'false') 2 | 3 | export const str2bool = (value: string) => value === 'true' 4 | -------------------------------------------------------------------------------- /src/shared/lib/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-is-mobile' 2 | export * from './use-media-query' 3 | export * from './use-event' 4 | export * from './use-toggle' 5 | export * from './use-event-listener' 6 | -------------------------------------------------------------------------------- /src/shared/lib/hooks/use-event-listener/index.ts: -------------------------------------------------------------------------------- 1 | //https://usehooks-ts.com/react-hook/use-event-listener 2 | import { useEffect, useLayoutEffect, useRef } from 'react' 3 | import type { RefObject } from 'react' 4 | 5 | // MediaQueryList Event based useEventListener interface 6 | function useEventListener( 7 | eventName: K, 8 | handler: (event: MediaQueryListEventMap[K]) => void, 9 | element: RefObject, 10 | options?: boolean | AddEventListenerOptions 11 | ): void 12 | 13 | // Window Event based useEventListener interface 14 | function useEventListener( 15 | eventName: K, 16 | handler: (event: WindowEventMap[K]) => void, 17 | element?: undefined, 18 | options?: boolean | AddEventListenerOptions 19 | ): void 20 | 21 | // Element Event based useEventListener interface 22 | function useEventListener< 23 | K extends keyof HTMLElementEventMap & keyof SVGElementEventMap, 24 | T extends Element = K extends keyof HTMLElementEventMap 25 | ? HTMLDivElement 26 | : SVGElement, 27 | >( 28 | eventName: K, 29 | handler: 30 | | ((event: HTMLElementEventMap[K]) => void) 31 | | ((event: SVGElementEventMap[K]) => void), 32 | element: RefObject, 33 | options?: boolean | AddEventListenerOptions 34 | ): void 35 | 36 | // Document Event based useEventListener interface 37 | function useEventListener( 38 | eventName: K, 39 | handler: (event: DocumentEventMap[K]) => void, 40 | element: RefObject, 41 | options?: boolean | AddEventListenerOptions 42 | ): void 43 | 44 | function useEventListener< 45 | KW extends keyof WindowEventMap, 46 | KH extends keyof HTMLElementEventMap & keyof SVGElementEventMap, 47 | KM extends keyof MediaQueryListEventMap, 48 | T extends HTMLElement | SVGAElement | MediaQueryList = HTMLElement, 49 | >( 50 | eventName: KW | KH | KM, 51 | handler: ( 52 | event: 53 | | WindowEventMap[KW] 54 | | HTMLElementEventMap[KH] 55 | | SVGElementEventMap[KH] 56 | | MediaQueryListEventMap[KM] 57 | | Event 58 | ) => void, 59 | element?: RefObject, 60 | options?: boolean | AddEventListenerOptions 61 | ) { 62 | // Create a ref that stores handler 63 | const savedHandler = useRef(handler) 64 | 65 | useLayoutEffect(() => { 66 | savedHandler.current = handler 67 | }, [handler]) 68 | 69 | useEffect(() => { 70 | // Define the listening target 71 | const targetElement: T | Window = element?.current ?? window 72 | 73 | if (!(targetElement && targetElement.addEventListener)) return 74 | 75 | // Create event listener that calls handler function stored in ref 76 | const listener: typeof handler = (event) => { 77 | savedHandler.current(event) 78 | } 79 | 80 | targetElement.addEventListener(eventName, listener, options) 81 | 82 | // Remove event listener on cleanup 83 | return () => { 84 | targetElement.removeEventListener(eventName, listener, options) 85 | } 86 | }, [eventName, element, options]) 87 | } 88 | 89 | export { useEventListener } 90 | -------------------------------------------------------------------------------- /src/shared/lib/hooks/use-event/index.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react' 2 | 3 | export const useEvent = (fn: T): T => { 4 | const fnRef = useRef(fn) 5 | 6 | useEffect(() => { 7 | fnRef.current = fn 8 | }, [fn]) 9 | 10 | const eventCb = useCallback( 11 | (...args: unknown[]) => { 12 | return fnRef.current.apply(null, args) 13 | }, 14 | [fnRef] 15 | ) 16 | 17 | return eventCb as unknown as T 18 | } 19 | -------------------------------------------------------------------------------- /src/shared/lib/hooks/use-is-mobile/index.ts: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@mui/joy' 2 | 3 | import { useMediaQuery } from '../use-media-query' 4 | 5 | export const useIsMobile = () => { 6 | const theme = useTheme() 7 | const matches = useMediaQuery( 8 | theme.breakpoints.down('md').split(' ').at(-1) as any 9 | ) 10 | 11 | return matches 12 | } 13 | -------------------------------------------------------------------------------- /src/shared/lib/hooks/use-media-query/index.ts: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useState } from 'react' 2 | 3 | type UseMediaQueryOptions = { 4 | defaultValue?: boolean 5 | initializeWithValue?: boolean 6 | } 7 | 8 | const IS_SERVER = typeof window === 'undefined' 9 | 10 | export function useMediaQuery( 11 | query: string, 12 | { 13 | defaultValue = false, 14 | initializeWithValue = true, 15 | }: UseMediaQueryOptions = {} 16 | ): boolean { 17 | const getMatches = (query: string): boolean => { 18 | if (IS_SERVER) { 19 | return defaultValue 20 | } 21 | return window.matchMedia(query).matches 22 | } 23 | 24 | const [matches, setMatches] = useState(() => { 25 | if (initializeWithValue) { 26 | return getMatches(query) 27 | } 28 | return defaultValue 29 | }) 30 | 31 | // Handles the change event of the media query. 32 | function handleChange() { 33 | setMatches(getMatches(query)) 34 | } 35 | 36 | useLayoutEffect(() => { 37 | const matchMedia = window.matchMedia(query) 38 | 39 | // Triggered at the first client-side load and if query changes 40 | handleChange() 41 | 42 | // Use deprecated `addListener` and `removeListener` to support Safari < 14 (#135) 43 | if (matchMedia.addListener) { 44 | matchMedia.addListener(handleChange) 45 | } else { 46 | matchMedia.addEventListener('change', handleChange) 47 | } 48 | 49 | return () => { 50 | if (matchMedia.removeListener) { 51 | matchMedia.removeListener(handleChange) 52 | } else { 53 | matchMedia.removeEventListener('change', handleChange) 54 | } 55 | } 56 | }, [query]) 57 | 58 | return matches 59 | } 60 | -------------------------------------------------------------------------------- /src/shared/lib/hooks/use-toggle/index.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { useEvent } from '../use-event' 4 | 5 | export const useToggle = (initialState = false) => { 6 | const [state, setState] = useState(initialState) 7 | const toggle = useEvent(() => setState((prev) => !prev)) 8 | return [state, toggle] as const 9 | } 10 | -------------------------------------------------------------------------------- /src/shared/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './nextjs' 2 | export * from './hooks' 3 | export * from './boolean' 4 | export * from './modal' 5 | -------------------------------------------------------------------------------- /src/shared/lib/modal/index.ts: -------------------------------------------------------------------------------- 1 | import { createEvent, createStore } from 'effector' 2 | import { useUnit } from 'effector-react' 3 | 4 | export const createDialogApi = () => { 5 | const $open = createStore(false) 6 | 7 | const toggleOpenEvent = createEvent() 8 | 9 | $open.on(toggleOpenEvent, (value) => !value) 10 | 11 | return { 12 | $open, 13 | toggleOpenEvent, 14 | } 15 | } 16 | 17 | export const useDialogApi = (api: ReturnType) => { 18 | const [open, toggleOpen] = useUnit([api.$open, api.toggleOpenEvent]) 19 | 20 | return { 21 | open, 22 | toggleOpen, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/shared/lib/nextjs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './no-ssr' 2 | -------------------------------------------------------------------------------- /src/shared/lib/nextjs/no-ssr/index.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | import React, { PropsWithChildren } from 'react' 3 | 4 | const _NoSsr = (props: PropsWithChildren) => ( 5 | {props.children} 6 | ) 7 | 8 | export const NoSSR = dynamic(() => Promise.resolve(_NoSsr), { 9 | ssr: false, 10 | }) 11 | -------------------------------------------------------------------------------- /src/shared/ui/drawer/index.tsx: -------------------------------------------------------------------------------- 1 | //https://codesandbox.io/p/sandbox/drawer-joy-ui-fnqxzp?file=%2FDrawer.tsx%3A1%2C1-81%2C1 2 | import Modal, { ModalProps, modalClasses } from '@mui/joy/Modal' 3 | import Sheet from '@mui/joy/Sheet' 4 | import React from 'react' 5 | 6 | export interface DrawerProps extends Omit { 7 | children: React.ReactNode 8 | 9 | size?: number | string 10 | position?: 'left' | 'right' | 'top' | 'bottom' 11 | } 12 | 13 | export function Drawer({ 14 | children, 15 | position = 'left', 16 | size = 'clamp(256px, 30vw, 378px)', 17 | sx, 18 | ...props 19 | }: DrawerProps) { 20 | return ( 21 | 36 | 62 | {children} 63 | 64 | 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /src/shared/ui/index.ts: -------------------------------------------------------------------------------- 1 | export * from './drawer' 2 | export * from './snackbar' 3 | -------------------------------------------------------------------------------- /src/shared/ui/snackbar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types' 2 | export * from './lib' 3 | export * from './ui' 4 | -------------------------------------------------------------------------------- /src/shared/ui/snackbar/lib/index.ts: -------------------------------------------------------------------------------- 1 | import { useUnit } from 'effector-react' 2 | 3 | import { addSnackbar } from '../model' 4 | 5 | export const useSnackbar = () => { 6 | const showSnackbar = useUnit(addSnackbar) 7 | 8 | return { showSnackbar } 9 | } 10 | -------------------------------------------------------------------------------- /src/shared/ui/snackbar/model/index.ts: -------------------------------------------------------------------------------- 1 | import { createEvent, createStore } from 'effector' 2 | 3 | import { ISnackbarItem } from '../types' 4 | 5 | export const $snackbar = createStore([]) 6 | 7 | export const addSnackbar = createEvent>() 8 | 9 | export const deleteSnackbar = createEvent() 10 | 11 | $snackbar 12 | .on(addSnackbar, (items, { message, type }) => [ 13 | ...items, 14 | { 15 | id: Math.random(), 16 | message, 17 | type, 18 | }, 19 | ]) 20 | .on(deleteSnackbar, (items, id) => items.filter((x) => x.id !== id)) 21 | -------------------------------------------------------------------------------- /src/shared/ui/snackbar/types.ts: -------------------------------------------------------------------------------- 1 | export interface ISnackbarItem { 2 | id: number 3 | message: string 4 | type: SnackbarType 5 | } 6 | 7 | export type SnackbarType = 'danger' | 'success' 8 | -------------------------------------------------------------------------------- /src/shared/ui/snackbar/ui/index.tsx: -------------------------------------------------------------------------------- 1 | import { useUnit } from 'effector-react' 2 | import { PropsWithChildren } from 'react' 3 | 4 | import { SnackbarItem } from './item' 5 | 6 | import { $snackbar, deleteSnackbar } from '../model' 7 | 8 | const SnackbarList = () => { 9 | const [snackbar, _deleteSnackbar] = useUnit([$snackbar, deleteSnackbar]) 10 | 11 | return snackbar.map((x) => ( 12 | 13 | )) 14 | } 15 | 16 | export const SnackbarProvider = ({ children }: PropsWithChildren) => { 17 | return ( 18 | <> 19 | {children} 20 | 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/shared/ui/snackbar/ui/item/index.tsx: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | import { UilCheck, UilExclamationTriangle } from '@iconscout/react-unicons' 3 | import { ModalClose, Snackbar } from '@mui/joy' 4 | import { useEffect, useState } from 'react' 5 | 6 | import { ISnackbarItem } from '../../types' 7 | 8 | interface IProps { 9 | item: ISnackbarItem 10 | deleteSnackbar: (id: number) => void 11 | } 12 | 13 | const autoHideDuration = 750 14 | const hideAnimationDuration = 100 15 | 16 | export const SnackbarItem = ({ item, deleteSnackbar }: IProps) => { 17 | const [open, setOpen] = useState(true) 18 | 19 | const handleClose = () => setOpen(false) 20 | 21 | useEffect(() => { 22 | const timeout = setTimeout(() => { 23 | setOpen(false) 24 | 25 | setTimeout(() => deleteSnackbar(item.id), hideAnimationDuration) 26 | }, autoHideDuration) 27 | 28 | return () => clearTimeout(timeout) 29 | }, [deleteSnackbar]) 30 | 31 | const Icon = item.type === 'success' ? UilCheck : UilExclamationTriangle 32 | 33 | return ( 34 | } 42 | endDecorator={ 43 | 49 | } 50 | > 51 | {item.message} 52 | 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /src/widgets/column/index.ts: -------------------------------------------------------------------------------- 1 | export * from './list' 2 | -------------------------------------------------------------------------------- /src/widgets/column/list/index.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from '@mui/joy' 2 | import { useUnit } from 'effector-react' 3 | import { COLUMN_ITEM_WIDTH } from 'entities/column' 4 | import { $selectedTable, $selectedTableIndex } from 'entities/table' 5 | import { setOpenedTaskOptionsEvent } from 'entities/task' 6 | import { 7 | $swipeIndex, 8 | ColumnItem, 9 | ColumnSwitcher, 10 | setListWidthEvent, 11 | } from 'features/column' 12 | import { useLayoutEffect, useRef } from 'react' 13 | 14 | export const ColumnList = () => { 15 | const ref = useRef(null) 16 | 17 | const [table, tableIndex, setOpenedTaskOptions, setListWidth, swipeIndex] = 18 | useUnit([ 19 | $selectedTable, 20 | $selectedTableIndex, 21 | setOpenedTaskOptionsEvent, 22 | setListWidthEvent, 23 | $swipeIndex, 24 | ]) 25 | 26 | useLayoutEffect(() => { 27 | const element = ref.current 28 | 29 | if (!element) { 30 | return 31 | } 32 | 33 | const onResize = () => { 34 | setListWidth(element.clientWidth) 35 | } 36 | onResize() 37 | window.addEventListener('resize', onResize) 38 | 39 | return () => window.removeEventListener('resize', onResize) 40 | }, [ref]) 41 | 42 | if (!table) { 43 | return null 44 | } 45 | 46 | return ( 47 | <> 48 | 49 | 59 | {table.columns.map((x, i) => ( 60 | { 65 | setOpenedTaskOptions({ 66 | tableIndex, 67 | columnIndex: i, 68 | taskIndex, 69 | }) 70 | }} 71 | /> 72 | ))} 73 | 74 | 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /src/widgets/header/constants.ts: -------------------------------------------------------------------------------- 1 | export const HEADER_HEIGHT = 80 2 | export const HEADER_HEIGHT_MOBILE = 56 3 | -------------------------------------------------------------------------------- /src/widgets/header/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants' 2 | export * from './ui' 3 | -------------------------------------------------------------------------------- /src/widgets/header/ui/actions/create-task-button/index.tsx: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | import { UilPlus } from '@iconscout/react-unicons' 3 | import { Button, IconButton } from '@mui/joy' 4 | 5 | import { createTaskDialogModel } from 'features/task' 6 | 7 | import { useDialogApi, useIsMobile } from 'shared/lib' 8 | 9 | export const CreateTaskButton = () => { 10 | const { toggleOpen } = useDialogApi(createTaskDialogModel) 11 | 12 | const isMobile = useIsMobile() 13 | 14 | return isMobile ? ( 15 | 16 | 17 | 18 | ) : ( 19 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/widgets/header/ui/actions/index.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from '@mui/joy' 2 | 3 | import { CreateTaskButton } from './create-task-button' 4 | import { MenuButton } from './menu-button' 5 | import { useSelectedTable } from 'entities/table' 6 | 7 | export const HeaderActions = () => { 8 | const { table } = useSelectedTable() 9 | 10 | if (!table) { 11 | return 12 | } 13 | 14 | return ( 15 | 16 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/widgets/header/ui/actions/menu-button/index.tsx: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | import { UilEditAlt, UilEllipsisV, UilTrash } from '@iconscout/react-unicons' 3 | 4 | import { 5 | MenuItem, 6 | MenuButton as _MenuButton, 7 | Menu, 8 | ListItemDecorator, 9 | IconButton, 10 | Dropdown, 11 | } from '@mui/joy' 12 | 13 | import { useDialogApi } from 'shared/lib' 14 | import { useSelectedTable } from 'entities/table' 15 | import { deleteTableDialogModel, updateTableDialogModel } from 'features/table' 16 | 17 | export const MenuButton = () => { 18 | const { table } = useSelectedTable() 19 | 20 | const { toggleOpen: toggleOpenUpdateDialog } = useDialogApi( 21 | updateTableDialogModel 22 | ) 23 | const { toggleOpen: toggleOpenDeleteDialog } = useDialogApi( 24 | deleteTableDialogModel 25 | ) 26 | 27 | if (!table) { 28 | return 29 | } 30 | 31 | return ( 32 | 33 | <_MenuButton 34 | slots={{ root: IconButton }} 35 | slotProps={{ root: { variant: 'outlined', color: 'neutral' } }} 36 | > 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | Edit table 45 | 46 | 51 | 52 | 53 | 54 | Delete table 55 | 56 | 57 | 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /src/widgets/header/ui/index.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@mui/joy' 2 | 3 | import { HeaderActions } from './actions' 4 | 5 | import { HeaderTitle } from './title' 6 | import { useLayoutIsExpanded } from 'widgets/layout' 7 | import { SIDEBAR_WIDTH } from 'widgets/sidebar' 8 | import { HEADER_HEIGHT, HEADER_HEIGHT_MOBILE } from '../constants' 9 | 10 | export const Header = () => { 11 | const { isExpanded } = useLayoutIsExpanded() 12 | 13 | return ( 14 | 15 | 16 | 17 | 18 | ) 19 | } 20 | 21 | const Root = styled('header')<{ isExpanded: boolean }>( 22 | ({ theme, isExpanded }) => ({ 23 | position: 'fixed', 24 | top: '0', 25 | left: isExpanded ? SIDEBAR_WIDTH : 0, 26 | right: '0', 27 | display: 'flex', 28 | alignItems: 'center', 29 | justifyContent: 'space-between', 30 | backgroundColor: theme.palette.background.surface, 31 | height: HEADER_HEIGHT, 32 | borderBottom: '1px solid #4b5563', 33 | padding: theme.spacing(0, 3), 34 | [theme.breakpoints.down('md')]: { 35 | left: 0, 36 | height: HEADER_HEIGHT_MOBILE, 37 | }, 38 | zIndex: 999, 39 | }) 40 | ) 41 | -------------------------------------------------------------------------------- /src/widgets/header/ui/title/index.tsx: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | import { UilAngleDown } from '@iconscout/react-unicons' 3 | import { Stack, styled } from '@mui/joy' 4 | 5 | import { useSelectedTable } from 'entities/table' 6 | 7 | import { useIsMobile, useToggle } from 'shared/lib' 8 | import { MobileMenu } from 'widgets/mobile-menu' 9 | 10 | const TITLE = 'Task management' 11 | 12 | export const HeaderTitle = () => { 13 | const isMobile = useIsMobile() 14 | const [open, toggleOpen] = useToggle(false) 15 | 16 | const { table } = useSelectedTable() 17 | 18 | const title = table ? table.title : TITLE 19 | 20 | if (isMobile) { 21 | return ( 22 | <> 23 | 30 | {title} 31 | 32 | 33 | 34 | 35 | ) 36 | } 37 | 38 | return {title} 39 | } 40 | 41 | const Title = styled('h1')(({ theme }) => ({ 42 | fontSize: theme.typography['title-lg'].fontSize, 43 | fontWeight: 'bold', 44 | color: theme.palette.text.primary, 45 | })) 46 | -------------------------------------------------------------------------------- /src/widgets/layout/constants.ts: -------------------------------------------------------------------------------- 1 | export const MAIN_CONTENT_ID = 'contentId' 2 | -------------------------------------------------------------------------------- /src/widgets/layout/index.ts: -------------------------------------------------------------------------------- 1 | export * from './model' 2 | export * from './lib' 3 | export * from './constants' 4 | export * from './ui' 5 | -------------------------------------------------------------------------------- /src/widgets/layout/lib.ts: -------------------------------------------------------------------------------- 1 | import { useUnit } from 'effector-react' 2 | import { $isExpanded } from './model' 3 | 4 | export const useLayoutIsExpanded = () => { 5 | const [isExpanded] = useUnit([$isExpanded]) 6 | 7 | return { 8 | isExpanded, 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/widgets/layout/model.ts: -------------------------------------------------------------------------------- 1 | import { createEvent, createStore } from 'effector' 2 | 3 | export const $isExpanded = createStore(true) 4 | 5 | export const toggleExpandedEvent = createEvent() 6 | 7 | $isExpanded.on(toggleExpandedEvent, (value) => !value) 8 | -------------------------------------------------------------------------------- /src/widgets/layout/ui/index.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@mui/joy' 2 | 3 | import { PropsWithChildren } from 'react' 4 | 5 | import { useLayoutIsExpanded } from '../lib' 6 | import { HEADER_HEIGHT, HEADER_HEIGHT_MOBILE, Header } from 'widgets/header' 7 | import { SIDEBAR_WIDTH, Sidebar } from 'widgets/sidebar' 8 | import { MAIN_CONTENT_ID } from '../constants' 9 | import { SidebarToggleButton } from './toggle-button' 10 | 11 | export const RootLayout = ({ children }: PropsWithChildren) => { 12 | const { isExpanded } = useLayoutIsExpanded() 13 | 14 | return ( 15 | 16 |
17 | 18 | 19 | {children} 20 | 21 | ) 22 | } 23 | 24 | const Root = styled('div')<{ isExpanded: boolean }>( 25 | ({ theme, isExpanded }) => ({ 26 | display: 'flex', 27 | flexDirection: 'column', 28 | minHeight: '100vh', 29 | paddingTop: HEADER_HEIGHT, 30 | paddingLeft: isExpanded ? SIDEBAR_WIDTH : 0, 31 | [theme.breakpoints.down('md')]: { 32 | paddingTop: HEADER_HEIGHT_MOBILE, 33 | paddingLeft: 0, 34 | }, 35 | overflowX: 'hidden', 36 | }) 37 | ) 38 | 39 | const MainContent = styled('main')(({ theme }) => ({ 40 | display: 'flex', 41 | alignItems: 'stretch', 42 | flexGrow: 1, 43 | padding: theme.spacing(3), 44 | })) 45 | -------------------------------------------------------------------------------- /src/widgets/layout/ui/toggle-button/index.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { UilEye, UilEyeSlash } from '@iconscout/react-unicons' 3 | import { IconButton, styled } from '@mui/joy' 4 | import { useUnit } from 'effector-react' 5 | 6 | import { bool2str, str2bool, useIsMobile } from 'shared/lib' 7 | import { toggleExpandedEvent, useLayoutIsExpanded } from 'widgets/layout' 8 | import { SIDEBAR_WIDTH } from 'widgets/sidebar' 9 | 10 | export const SidebarToggleButton = () => { 11 | const { isExpanded } = useLayoutIsExpanded() 12 | 13 | const toggleSidebar = useUnit(toggleExpandedEvent) 14 | 15 | const isMobile = useIsMobile() 16 | 17 | const Icon = isExpanded ? UilEyeSlash : UilEye 18 | 19 | if (isMobile) { 20 | return null 21 | } 22 | 23 | return ( 24 | 29 | 30 | 31 | ) 32 | } 33 | 34 | const Root = styled(IconButton)<{ is_expanded: string }>(({ is_expanded }) => ({ 35 | position: 'fixed', 36 | bottom: 22, 37 | left: str2bool(is_expanded) ? SIDEBAR_WIDTH : 0, 38 | borderTopLeftRadius: 0, 39 | borderBottomLeftRadius: 0, 40 | zIndex: 999, 41 | })) 42 | -------------------------------------------------------------------------------- /src/widgets/logo/index.tsx: -------------------------------------------------------------------------------- 1 | import { styled, useColorScheme } from '@mui/joy' 2 | 3 | import Image from 'next/image' 4 | import { brandLogo } from 'shared/assets/images' 5 | 6 | interface IProps { 7 | className?: string 8 | } 9 | 10 | export const Logo = ({ className }: IProps) => { 11 | const { mode } = useColorScheme() 12 | 13 | const isDarkMode = mode === 'dark' 14 | 15 | return ( 16 | 25 | ) 26 | } 27 | 28 | const Root = styled(Image)({}) 29 | -------------------------------------------------------------------------------- /src/widgets/mobile-menu/index.tsx: -------------------------------------------------------------------------------- 1 | import { ModalClose, styled } from '@mui/joy' 2 | import { Drawer } from 'shared/ui' 3 | import { Logo } from 'widgets/logo' 4 | import { TableList } from 'widgets/table' 5 | import { ThemeSwitchCard } from 'widgets/theme' 6 | 7 | interface IProps { 8 | open: boolean 9 | onClose: VoidFunction 10 | } 11 | 12 | export const MobileMenu = ({ open, onClose }: IProps) => { 13 | return ( 14 | 15 | 16 | 17 | <_ModelClose variant="soft" /> 18 | 19 | <_TableList /> 20 | <_ThemeSwitchCard /> 21 | 22 | ) 23 | } 24 | 25 | const Row = styled('div')(({ theme }) => ({ 26 | display: 'flex', 27 | flexDirection: 'row', 28 | alignItems: 'center', 29 | justifyContent: 'space-between', 30 | margin: theme.spacing(2, 2, 0, 2), 31 | })) 32 | 33 | const _ModelClose = styled(ModalClose)({ 34 | margin: 0, 35 | position: 'static', 36 | }) 37 | 38 | const _TableList = styled(TableList)(({ theme }) => ({ 39 | margin: theme.spacing(1, 0), 40 | })) 41 | 42 | const _ThemeSwitchCard = styled(ThemeSwitchCard)(({ theme }) => ({ 43 | margin: theme.spacing(0, 2, 2, 2), 44 | })) 45 | -------------------------------------------------------------------------------- /src/widgets/sidebar/constants.ts: -------------------------------------------------------------------------------- 1 | export const SIDEBAR_WIDTH = 256 2 | -------------------------------------------------------------------------------- /src/widgets/sidebar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants' 2 | export * from './ui' 3 | -------------------------------------------------------------------------------- /src/widgets/sidebar/ui/index.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@mui/joy' 2 | import { useIsMobile } from 'shared/lib' 3 | import { useLayoutIsExpanded } from 'widgets/layout' 4 | import { SIDEBAR_WIDTH } from '../constants' 5 | import { Logo } from 'widgets/logo' 6 | import { TableList } from 'widgets/table' 7 | import { ThemeSwitchCard } from 'widgets/theme' 8 | 9 | export const Sidebar = () => { 10 | const { isExpanded } = useLayoutIsExpanded() 11 | 12 | const isMobile = useIsMobile() 13 | 14 | if (isMobile) { 15 | return null 16 | } 17 | 18 | return ( 19 | 20 | <_Logo /> 21 | <_TableList /> 22 | <_ThemeSwitchCard /> 23 | 24 | ) 25 | } 26 | 27 | const Root = styled('aside')<{ isExpanded: boolean }>( 28 | ({ theme, isExpanded }) => ({ 29 | display: 'flex', 30 | flexDirection: 'column', 31 | justifyContent: 'space-between', 32 | position: 'fixed', 33 | top: '0', 34 | left: '0', 35 | bottom: '0', 36 | width: SIDEBAR_WIDTH, 37 | backgroundColor: theme.palette.background.surface, 38 | borderRight: '1px solid #4b5563', 39 | transform: `translateX(${isExpanded ? 0 : -SIDEBAR_WIDTH}px)`, 40 | padding: theme.spacing(2, 0), 41 | zIndex: 999, 42 | }) 43 | ) 44 | 45 | const _Logo = styled(Logo)(({ theme }) => ({ 46 | margin: theme.spacing(0, 2), 47 | })) 48 | 49 | const _TableList = styled(TableList)(({ theme }) => ({ 50 | margin: theme.spacing(1, 0, 0, 0), 51 | })) 52 | 53 | const _ThemeSwitchCard = styled(ThemeSwitchCard)(({ theme }) => ({ 54 | margin: theme.spacing(0, 2), 55 | })) 56 | -------------------------------------------------------------------------------- /src/widgets/table/index.ts: -------------------------------------------------------------------------------- 1 | export * from './list' 2 | -------------------------------------------------------------------------------- /src/widgets/table/list/create-table-button/index.tsx: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | import { UilAngleRightB, UilPlus } from '@iconscout/react-unicons' 3 | import { 4 | ListItem, 5 | ListItemButton, 6 | ListItemContent, 7 | ListItemDecorator, 8 | styled, 9 | } from '@mui/joy' 10 | import { createTableDialogModel } from 'features/table' 11 | 12 | import { useDialogApi } from 'shared/lib' 13 | 14 | export const CreateTableButton = () => { 15 | const { toggleOpen } = useDialogApi(createTableDialogModel) 16 | 17 | return ( 18 | <> 19 | <_ListItem> 20 | <_ListItemButton color="primary" onClick={toggleOpen}> 21 | 22 | 23 | 24 | Create new table 25 | 26 | 27 | 28 | 29 | ) 30 | } 31 | 32 | const _ListItem = styled(ListItem)(({ theme }) => ({})) 33 | 34 | const _ListItemButton = styled(ListItemButton)(({ theme }) => ({ 35 | padding: theme.spacing(1, 2), 36 | })) 37 | -------------------------------------------------------------------------------- /src/widgets/table/list/index.tsx: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | import { UilAngleRightB, UilFileAlt } from '@iconscout/react-unicons' 3 | import { 4 | List, 5 | ListItem, 6 | ListItemButton, 7 | ListItemContent, 8 | ListItemDecorator, 9 | styled, 10 | } from '@mui/joy' 11 | 12 | import { useTableList } from './model' 13 | import { CreateTableButton } from './create-table-button' 14 | 15 | interface IProps { 16 | className?: string 17 | } 18 | 19 | export const TableList = ({ className }: IProps) => { 20 | const { tables, isSelected, onClick } = useTableList() 21 | 22 | return ( 23 | 24 | {tables.map((x, i) => ( 25 | <_ListItem key={x.id}> 26 | <_ListItemButton 27 | variant={isSelected(i) ? 'soft' : 'plain'} 28 | onClick={onClick(i)} 29 | > 30 | 31 | 32 | 33 | {x.title} 34 | 35 | 36 | 37 | ))} 38 | 39 | 40 | ) 41 | } 42 | 43 | const _ListItem = styled(ListItem)(({ theme }) => ({})) 44 | 45 | const _ListItemButton = styled(ListItemButton)(({ theme }) => ({ 46 | padding: theme.spacing(1, 2), 47 | })) 48 | -------------------------------------------------------------------------------- /src/widgets/table/list/model.ts: -------------------------------------------------------------------------------- 1 | import { useUnit } from 'effector-react' 2 | import { $selectedTableIndex, $tables } from 'entities/table' 3 | import { setSelectedTableIndexEvent } from 'features/table' 4 | 5 | export const useTableList = () => { 6 | const [tables, selectedIndex, setSelectedIndex] = useUnit([ 7 | $tables, 8 | $selectedTableIndex, 9 | setSelectedTableIndexEvent, 10 | ]) 11 | 12 | const isSelected = (index: number) => { 13 | return selectedIndex === index 14 | } 15 | 16 | const handleOnClick = (index: number) => { 17 | return () => setSelectedTableIndexEvent(index) 18 | } 19 | 20 | return { 21 | tables, 22 | isSelected, 23 | onClick: handleOnClick, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/widgets/task/detial/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ui' 2 | -------------------------------------------------------------------------------- /src/widgets/task/detial/model.ts: -------------------------------------------------------------------------------- 1 | import { useUnit } from 'effector-react' 2 | import { $tables } from 'entities/table' 3 | import { $openedTaskOptions, setOpenedTaskOptionsEvent } from 'entities/task' 4 | import { useMemo } from 'react' 5 | 6 | export const useTaskDetail = () => { 7 | const [tables, openedTaskOptions, setOpenedTaskOptions] = useUnit([ 8 | $tables, 9 | $openedTaskOptions, 10 | setOpenedTaskOptionsEvent, 11 | ]) 12 | 13 | const handleOnClose = () => { 14 | setOpenedTaskOptions(null) 15 | } 16 | 17 | const task = useMemo(() => { 18 | return openedTaskOptions 19 | ? tables[openedTaskOptions.tableIndex].columns[ 20 | openedTaskOptions.columnIndex 21 | ].tasks[openedTaskOptions.taskIndex] 22 | : null 23 | }, [ 24 | tables, 25 | openedTaskOptions?.tableIndex, 26 | openedTaskOptions?.columnIndex, 27 | openedTaskOptions?.taskIndex, 28 | ]) 29 | 30 | return { 31 | open: Boolean(openedTaskOptions), 32 | onClose: handleOnClose, 33 | task, 34 | openedTaskOptions, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/widgets/task/detial/ui/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DialogTitle, 3 | Modal, 4 | ModalClose, 5 | ModalDialog, 6 | ModalOverflow, 7 | Stack, 8 | Typography, 9 | } from '@mui/joy' 10 | 11 | import { TodoItem } from 'entities/todo' 12 | import { ColumnSelect } from 'features/column' 13 | import { ToggleTodo } from 'features/todo' 14 | import { MenuButton } from './menu-button' 15 | 16 | import { useTaskDetail } from '../model' 17 | 18 | export const ViewTaskDetail = () => { 19 | const { open, onClose, task, openedTaskOptions } = useTaskDetail() 20 | 21 | if (!open || !task) { 22 | return null 23 | } 24 | 25 | const { tableIndex, columnIndex, taskIndex } = openedTaskOptions! 26 | 27 | return ( 28 | 29 | 30 | 31 | 37 | {task!.title} 38 | 39 | 46 | 47 | 48 | 49 | 50 | {task!.description} 51 | 52 | 53 | {task!.todos.map((item, index) => ( 54 | 64 | } 65 | todo={item} 66 | /> 67 | ))} 68 | 69 | 74 | 75 | 76 | 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /src/widgets/task/detial/ui/menu-button/index.tsx: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | import { UilEditAlt, UilEllipsisV, UilTrash } from '@iconscout/react-unicons' 3 | 4 | import { 5 | MenuButton as _MenuButton, 6 | Menu, 7 | MenuItem, 8 | IconButton, 9 | Dropdown, 10 | ListItemDecorator, 11 | } from '@mui/joy' 12 | 13 | import { useUnit } from 'effector-react' 14 | import { 15 | setDeleteTaskOptionsEvent, 16 | setUpdateTaskOptionsEvent, 17 | } from 'features/task' 18 | import { ITask } from 'entities/task' 19 | 20 | interface IProps { 21 | tableIndex: number 22 | columnIndex: number 23 | taskIndex: number 24 | task: ITask 25 | onClose: VoidFunction 26 | } 27 | 28 | export const MenuButton = ({ 29 | tableIndex, 30 | columnIndex, 31 | taskIndex, 32 | task, 33 | onClose, 34 | }: IProps) => { 35 | const [setUpdateTaskOptions, setDeleteTaskOptions] = useUnit([ 36 | setUpdateTaskOptionsEvent, 37 | setDeleteTaskOptionsEvent, 38 | ]) 39 | 40 | const handleOpenUpdateDialog = () => { 41 | setUpdateTaskOptions({ 42 | tableIndex, 43 | columnIndex, 44 | taskIndex, 45 | }) 46 | } 47 | 48 | const handleOpenDeleteDialog = () => { 49 | onClose() 50 | setDeleteTaskOptions({ 51 | tableIndex, 52 | columnIndex, 53 | taskIndex, 54 | }) 55 | } 56 | 57 | return ( 58 | 59 | <_MenuButton 60 | slots={{ root: IconButton }} 61 | slotProps={{ root: { variant: 'outlined', color: 'neutral' } }} 62 | > 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | Edit task 71 | 72 | 77 | 78 | 79 | 80 | Delete task 81 | 82 | 83 | 84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /src/widgets/task/index.ts: -------------------------------------------------------------------------------- 1 | export * from './detial' 2 | -------------------------------------------------------------------------------- /src/widgets/theme/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ui' 2 | -------------------------------------------------------------------------------- /src/widgets/theme/ui.tsx: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | import { UilMoon, UilSun } from '@iconscout/react-unicons/' 3 | import { styled } from '@mui/joy' 4 | import { ThemeSwitch } from 'features/theme' 5 | 6 | interface IProps { 7 | className?: string 8 | } 9 | 10 | export const ThemeSwitchCard = ({ className }: IProps) => { 11 | return ( 12 | 13 | } endDecorator={} /> 14 | 15 | ) 16 | } 17 | 18 | export const Root = styled('div')(({ theme }) => ({ 19 | display: 'flex', 20 | alignItems: 'center', 21 | justifyContent: 'center', 22 | backgroundColor: theme.palette.background.level1, 23 | borderRadius: 8, 24 | padding: theme.spacing(1.5, 0), 25 | })) 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "paths": { 22 | "*": [ 23 | "./src/*" 24 | ] 25 | }, 26 | "plugins": [ 27 | { 28 | "name": "next" 29 | } 30 | ] 31 | }, 32 | "include": [ 33 | "next-env.d.ts", 34 | "**/*.ts", 35 | "**/*.tsx", 36 | ".next/types/**/*.ts" 37 | ], 38 | "exclude": [ 39 | "node_modules" 40 | ] 41 | } 42 | --------------------------------------------------------------------------------