├── .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 | 
10 | 
11 | 
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 |
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 |
49 | )
50 | })
51 |
52 | return <_CacheProvider value={cache}>{children}
53 | }
54 |
--------------------------------------------------------------------------------
/src/app/providers/dialogs/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | CreateTableDialog,
3 | DeleteTableDialog,
4 | UpdateTableDialog,
5 | } from 'features/table'
6 | import {
7 | CreateTaskDialog,
8 | DeleteTaskDialog,
9 | UpdateTaskDialog,
10 | } from 'features/task'
11 | import { PropsWithChildren } from 'react'
12 | import { ViewTaskDetail } from 'widgets/task'
13 |
14 | export const DialogsProvider = ({ children }: PropsWithChildren) => {
15 | return (
16 | <>
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {children}
25 | >
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/providers/index.tsx:
--------------------------------------------------------------------------------
1 | import { SnackbarProvider } from 'shared/ui'
2 | import { PropsWithChildren } from 'react'
3 |
4 | import { CacheProvider } from './cache'
5 | import { ThemeProvider } from './theme'
6 | import { DialogsProvider } from './dialogs'
7 |
8 | export const Providers = ({
9 | children,
10 | pageProps,
11 | }: PropsWithChildren<{ pageProps: any }>) => {
12 | return (
13 |
14 |
15 |
16 | {children}
17 |
18 |
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/providers/theme/config.ts:
--------------------------------------------------------------------------------
1 | import { extendTheme } from '@mui/joy'
2 |
3 | export const theme = extendTheme({
4 | colorSchemes: {
5 | light: {
6 | palette: {
7 | primary: {
8 | '50': '#eef2ff',
9 | '100': '#e0e7ff',
10 | '200': '#c7d2fe',
11 | '300': '#a5b4fc',
12 | '400': '#818cf8',
13 | '500': '#6366f1',
14 | '600': '#4f46e5',
15 | '700': '#4338ca',
16 | '800': '#3730a3',
17 | '900': '#312e81',
18 | },
19 | background: {
20 | body: '#f4f7fd',
21 | surface: '#fff',
22 | },
23 | text: {
24 | primary: '#000',
25 | },
26 | },
27 | },
28 | dark: {
29 | palette: {
30 | primary: {
31 | '50': '#eef2ff',
32 | '100': '#e0e7ff',
33 | '200': '#c7d2fe',
34 | '300': '#a5b4fc',
35 | '400': '#818cf8',
36 | '500': '#6366f1',
37 | '600': '#4f46e5',
38 | '700': '#4338ca',
39 | '800': '#3730a3',
40 | '900': '#312e81',
41 | },
42 | background: {
43 | body: '#20212c',
44 | surface: '#2b2c37',
45 | level1: '#20212c',
46 | },
47 | text: {
48 | primary: '#fff',
49 | },
50 | },
51 | },
52 | },
53 | })
54 |
--------------------------------------------------------------------------------
/src/app/providers/theme/index.tsx:
--------------------------------------------------------------------------------
1 | import { CssBaseline, CssVarsProvider } from '@mui/joy'
2 | import { PropsWithChildren } from 'react'
3 |
4 | import { theme } from './config'
5 |
6 | export const ThemeProvider = ({ children }: PropsWithChildren) => {
7 | return (
8 |
14 |
15 | {children}
16 |
17 | )
18 | }
19 |
20 | export * from './config'
21 |
--------------------------------------------------------------------------------
/src/app/styles/index.css:
--------------------------------------------------------------------------------
1 | *,
2 | *::after,
3 | *::before {
4 | margin: 0;
5 | padding: 0;
6 | box-sizing: border-box;
7 | }
8 |
--------------------------------------------------------------------------------
/src/entities/column/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types'
2 | export * from './ui'
3 |
--------------------------------------------------------------------------------
/src/entities/column/types.ts:
--------------------------------------------------------------------------------
1 | import { ITask } from 'entities/task'
2 |
3 | export interface IColumn {
4 | id: string
5 | title: string
6 | tasks: ITask[]
7 | tableId: string
8 | }
9 |
--------------------------------------------------------------------------------
/src/entities/column/ui/index.ts:
--------------------------------------------------------------------------------
1 | export * from './item'
2 |
--------------------------------------------------------------------------------
/src/entities/column/ui/item/constants.ts:
--------------------------------------------------------------------------------
1 | export const COLUMN_ITEM_WIDTH = 280
2 |
--------------------------------------------------------------------------------
/src/entities/column/ui/item/index.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Stack, Typography } from '@mui/joy'
2 | import { IColumn } from 'entities/column/types'
3 | import { ITask } from 'entities/task'
4 | import { ReactNode } from 'react'
5 | import { COLUMN_ITEM_WIDTH } from './constants'
6 |
7 | interface IProps {
8 | column: IColumn
9 | renderTask: (task: ITask, index: number) => ReactNode
10 | onDrop?: any
11 | onDragOver?: any
12 | }
13 |
14 | export const ColumnCard = ({
15 | column,
16 | renderTask,
17 | onDrop,
18 | onDragOver,
19 | }: IProps) => {
20 | return (
21 |
26 |
27 | {`${column.title} (${column.tasks.length})`}
28 |
29 |
30 | {column.tasks.map((task, i) => renderTask(task, i))}
31 |
32 |
33 | )
34 | }
35 |
36 | export { COLUMN_ITEM_WIDTH }
37 |
--------------------------------------------------------------------------------
/src/entities/table/index.ts:
--------------------------------------------------------------------------------
1 | export * from './model'
2 | export * from './types'
3 | export * from './lib'
4 |
--------------------------------------------------------------------------------
/src/entities/table/lib.ts:
--------------------------------------------------------------------------------
1 | import { useUnit } from 'effector-react'
2 | import { $selectedTable } from './model'
3 |
4 | export const useSelectedTable = () => {
5 | const [table] = useUnit([$selectedTable])
6 |
7 | return { table }
8 | }
9 |
--------------------------------------------------------------------------------
/src/entities/table/model.ts:
--------------------------------------------------------------------------------
1 | import { combine, createStore } from 'effector'
2 | import { ITable } from './types'
3 | import data from 'shared/assets/data.json'
4 | import { persist } from 'effector-storage/local'
5 |
6 | export const $tables = createStore(data, { name: '@tables' })
7 | export const $selectedTableIndex = createStore(-1)
8 | export const $selectedTable = combine(
9 | $tables,
10 | $selectedTableIndex,
11 | (tables, index) => (tables[index] ? tables[index] : null)
12 | )
13 |
14 | persist({ store: $tables })
15 |
--------------------------------------------------------------------------------
/src/entities/table/types.ts:
--------------------------------------------------------------------------------
1 | import { IColumn } from 'entities/column'
2 |
3 | export interface ITable {
4 | id: string
5 | title: string
6 | columns: IColumn[]
7 | }
8 |
--------------------------------------------------------------------------------
/src/entities/task/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types'
2 | export * from './ui'
3 | export * from './lib'
4 | export * from './model'
5 |
--------------------------------------------------------------------------------
/src/entities/task/lib/index.ts:
--------------------------------------------------------------------------------
1 | import { useUnit } from 'effector-react'
2 | import { $tables } from 'entities/table'
3 | import { useMemo } from 'react'
4 |
5 | interface IProps {
6 | tableIndex: number
7 | columnIndex: number
8 | taskIndex: number
9 | }
10 |
11 | export const useTask = ({ tableIndex, columnIndex, taskIndex }: IProps) => {
12 | const [tables] = useUnit([$tables])
13 |
14 | const task = useMemo(() => {
15 | return tables[tableIndex].columns[columnIndex].tasks[taskIndex]
16 | }, [tables, tableIndex, columnIndex, taskIndex])
17 |
18 | return { task }
19 | }
20 |
--------------------------------------------------------------------------------
/src/entities/task/model.ts:
--------------------------------------------------------------------------------
1 | import { createEvent, createStore } from 'effector'
2 | import { ITaskOptions } from './types'
3 |
4 | export const $openedTaskOptions = createStore(null)
5 |
6 | export const setOpenedTaskOptionsEvent = createEvent()
7 |
8 | $openedTaskOptions.on(setOpenedTaskOptionsEvent, (_, value) => value)
9 |
--------------------------------------------------------------------------------
/src/entities/task/types.ts:
--------------------------------------------------------------------------------
1 | import { ITodo } from 'entities/todo'
2 |
3 | export interface ITaskOptions {
4 | tableIndex: number
5 | columnIndex: number
6 | taskIndex: number
7 | }
8 |
9 | export interface ITask {
10 | id: string
11 | title: string
12 | description: string
13 | todos: ITodo[]
14 | columnId: string
15 | }
16 |
--------------------------------------------------------------------------------
/src/entities/task/ui/card/index.tsx:
--------------------------------------------------------------------------------
1 | import { Card, LinearProgress, Typography } from '@mui/joy'
2 | import { ITask } from 'entities/task'
3 | import { getCompletedTaskCount } from './lib'
4 |
5 | interface IProps {
6 | task: ITask
7 | onClick?: VoidFunction
8 | onDragStart?: (e: any) => void
9 | draggable?: boolean
10 | }
11 |
12 | export const TaskCard = ({ task, onClick, onDragStart, draggable }: IProps) => {
13 | const todoCount = task.todos.length
14 | const doneCount = getCompletedTaskCount(task)
15 |
16 | return (
17 |
24 | {task.title}
25 | {`${doneCount} of ${todoCount} todos`}
26 | 0 ? (doneCount / task.todos.length) * 100 : 0}
29 | thickness={8}
30 | />
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/src/entities/task/ui/card/lib.ts:
--------------------------------------------------------------------------------
1 | import { ITask } from 'entities/task/'
2 |
3 | export const getCompletedTaskCount = (task: ITask) => {
4 | return task.todos.filter((x) => x.isDone).length
5 | }
6 |
--------------------------------------------------------------------------------
/src/entities/task/ui/index.ts:
--------------------------------------------------------------------------------
1 | export * from './card'
2 |
--------------------------------------------------------------------------------
/src/entities/todo/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types'
2 | export * from './item'
3 |
--------------------------------------------------------------------------------
/src/entities/todo/item/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ui'
2 |
--------------------------------------------------------------------------------
/src/entities/todo/item/ui/index.tsx:
--------------------------------------------------------------------------------
1 | import { Card, Typography, styled } from '@mui/joy'
2 | import { ITodo } from '../../types'
3 | import { ReactNode } from 'react'
4 |
5 | interface IProps {
6 | startDecorator?: ReactNode
7 | todo: ITodo
8 | className?: string
9 | }
10 |
11 | export const TodoItem = ({ startDecorator, todo, className }: IProps) => {
12 | return (
13 | <_Card className={className} variant="soft">
14 | {startDecorator}
15 | {todo.title}
16 |
17 | )
18 | }
19 |
20 | const _Card = styled(Card)(({ theme }) => ({
21 | display: 'flex',
22 | flexDirection: 'row',
23 | alignItems: 'center',
24 | padding: theme.spacing(1, 1),
25 | }))
26 |
--------------------------------------------------------------------------------
/src/entities/todo/types.ts:
--------------------------------------------------------------------------------
1 | export interface ITodo {
2 | id: string
3 | title: string
4 | isDone: boolean
5 | }
6 |
--------------------------------------------------------------------------------
/src/features/column/drag-and-drop/constants.ts:
--------------------------------------------------------------------------------
1 | export const DRAG_TRANSFER_KEY = 'column'
2 |
--------------------------------------------------------------------------------
/src/features/column/drag-and-drop/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ui'
2 |
--------------------------------------------------------------------------------
/src/features/column/drag-and-drop/model/index.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Un1T3G/nextjs-task-management-app/7315f8bdcfe9a7047bfcafed83e81f326f7eab76/src/features/column/drag-and-drop/model/index.ts
--------------------------------------------------------------------------------
/src/features/column/drag-and-drop/model/model.ts:
--------------------------------------------------------------------------------
1 | import { createEvent } from 'effector'
2 | import { IDropNewTaskEventProps } from '../types'
3 | import { $tables } from 'entities/table'
4 |
5 | export const dropNewTaskOnColumnEvent = createEvent()
6 |
7 | $tables.on(
8 | dropNewTaskOnColumnEvent,
9 | (tables, { fromColumnIndex, toColumnIndex, tableIndex, dropTaskIndex }) => {
10 | const task =
11 | tables[tableIndex].columns[fromColumnIndex].tasks[dropTaskIndex]
12 | task.columnId = tables[tableIndex].columns[toColumnIndex].id
13 |
14 | tables[tableIndex].columns[fromColumnIndex].tasks = tables[
15 | tableIndex
16 | ].columns[fromColumnIndex].tasks.filter((_, i) => i !== dropTaskIndex)
17 |
18 | tables[tableIndex].columns[toColumnIndex].tasks = [
19 | ...tables[tableIndex].columns[toColumnIndex].tasks,
20 | task,
21 | ]
22 |
23 | tables[tableIndex] = { ...tables[tableIndex] }
24 |
25 | return [...tables]
26 | }
27 | )
28 |
--------------------------------------------------------------------------------
/src/features/column/drag-and-drop/model/use-column-drag-and-drop.ts:
--------------------------------------------------------------------------------
1 | import { useUnit } from 'effector-react'
2 | import { DragEvent } from 'react'
3 | import { DRAG_TRANSFER_KEY } from '../constants'
4 | import { dropNewTaskOnColumnEvent } from './model'
5 | import { $selectedTableIndex } from 'entities/table'
6 |
7 | interface IProps {
8 | columnIndex: number
9 | }
10 |
11 | export const useColumnDragAndDrop = ({ columnIndex }: IProps) => {
12 | const handleOnDragStart = (index: number) => {
13 | return (e: DragEvent) => {
14 | const text = JSON.stringify({
15 | fromColumnIndex: columnIndex,
16 | dropTaskIndex: index,
17 | })
18 | e.dataTransfer.setData(DRAG_TRANSFER_KEY, text)
19 | }
20 | }
21 |
22 | const [tableIndex, dropNewTaskOnColumn] = useUnit([
23 | $selectedTableIndex,
24 | dropNewTaskOnColumnEvent,
25 | ])
26 |
27 | const handleOnDrop = (e: DragEvent) => {
28 | const data = e.dataTransfer.getData(DRAG_TRANSFER_KEY)
29 |
30 | if (!data) {
31 | return
32 | }
33 |
34 | const { fromColumnIndex, dropTaskIndex } = JSON.parse(data)
35 |
36 | if (fromColumnIndex === columnIndex) {
37 | return
38 | }
39 |
40 | dropNewTaskOnColumn({
41 | fromColumnIndex,
42 | toColumnIndex: columnIndex,
43 | tableIndex,
44 | dropTaskIndex,
45 | })
46 | }
47 |
48 | const handleOnDragOver = (e: DragEvent) => {
49 | e.preventDefault()
50 | }
51 |
52 | return {
53 | onDrop: handleOnDrop,
54 | onDragOver: handleOnDragOver,
55 | onDragStart: handleOnDragStart,
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/features/column/drag-and-drop/types.ts:
--------------------------------------------------------------------------------
1 | export interface IDropNewTaskEventProps {
2 | fromColumnIndex: number
3 | toColumnIndex: number
4 | tableIndex: number
5 | dropTaskIndex: number
6 | }
7 |
--------------------------------------------------------------------------------
/src/features/column/drag-and-drop/ui/index.tsx:
--------------------------------------------------------------------------------
1 | import { ColumnCard, IColumn } from 'entities/column'
2 | import { TaskCard } from 'entities/task'
3 | import { useColumnDragAndDrop } from '../model/use-column-drag-and-drop'
4 |
5 | interface IProps {
6 | column: IColumn
7 | columnIndex: number
8 | onClickTaskCard: (taskIndex: number) => void
9 | }
10 |
11 | export const ColumnItem = ({
12 | column,
13 | columnIndex,
14 | onClickTaskCard,
15 | }: IProps) => {
16 | const { onDragOver, onDragStart, onDrop } = useColumnDragAndDrop({
17 | columnIndex,
18 | })
19 |
20 | return (
21 |
24 | task && (
25 | onClickTaskCard(i)}
31 | />
32 | )
33 | }
34 | onDrop={onDrop}
35 | onDragOver={onDragOver}
36 | />
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/src/features/column/index.ts:
--------------------------------------------------------------------------------
1 | export * from './drag-and-drop'
2 | export * from './select'
3 | export * from './switcher'
4 |
--------------------------------------------------------------------------------
/src/features/column/select/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ui'
2 |
--------------------------------------------------------------------------------
/src/features/column/select/model/index.ts:
--------------------------------------------------------------------------------
1 | export * from './use-coloumn-select'
2 |
--------------------------------------------------------------------------------
/src/features/column/select/model/model.ts:
--------------------------------------------------------------------------------
1 | import { createEvent } from 'effector'
2 | import { IChangeTaskColumnEventProps } from '../types'
3 | import { $tables } from 'entities/table'
4 |
5 | export const changeTaskColumnEvent = createEvent()
6 |
7 | $tables.on(
8 | changeTaskColumnEvent,
9 | (tables, { fromColumnId, newColumnId, tableIndex, taskIndex }) => {
10 | const table = tables[tableIndex]
11 | const fromColumn = table.columns.find(
12 | (column) => column.id === fromColumnId
13 | )
14 | const newColumn = table.columns.find((column) => column.id === newColumnId)
15 |
16 | if (!fromColumn || !newColumn) {
17 | return tables
18 | }
19 |
20 | const task = fromColumn.tasks[taskIndex]
21 | task.columnId = newColumn.id
22 |
23 | fromColumn.tasks = fromColumn.tasks.filter((_, i) => i !== taskIndex)
24 | newColumn.tasks = [...newColumn.tasks, task]
25 |
26 | tables[tableIndex] = { ...table }
27 |
28 | return [...tables]
29 | }
30 | )
31 |
--------------------------------------------------------------------------------
/src/features/column/select/model/use-coloumn-select.ts:
--------------------------------------------------------------------------------
1 | import { useUnit } from 'effector-react'
2 | import { $selectedTable } from 'entities/table'
3 | import { changeTaskColumnEvent } from './model'
4 | import { useMemo } from 'react'
5 |
6 | interface IProps {
7 | columnId: string
8 | tableIndex: number
9 | taskIndex: number
10 | }
11 |
12 | export const useColumnSelect = ({
13 | columnId,
14 | tableIndex,
15 | taskIndex,
16 | }: IProps) => {
17 | const [table, changeTaskColumn] = useUnit([
18 | $selectedTable,
19 | changeTaskColumnEvent,
20 | ])
21 |
22 | const options = useMemo(() => {
23 | return table
24 | ? table.columns.map((x) => ({
25 | id: x.id,
26 | title: x.title,
27 | }))
28 | : []
29 | }, [table])
30 |
31 | const handleChange = (
32 | event: React.SyntheticEvent | null,
33 | newValue: string | null
34 | ) => {
35 | if (!newValue) {
36 | return
37 | }
38 |
39 | changeTaskColumn({
40 | fromColumnId: columnId,
41 | newColumnId: newValue,
42 | tableIndex,
43 | taskIndex,
44 | })
45 | }
46 |
47 | return {
48 | options,
49 | onChange: handleChange,
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/features/column/select/types.ts:
--------------------------------------------------------------------------------
1 | export interface IChangeTaskColumnEventProps {
2 | fromColumnId: string
3 | newColumnId: string
4 | tableIndex: number
5 | taskIndex: number
6 | }
7 |
--------------------------------------------------------------------------------
/src/features/column/select/ui/index.tsx:
--------------------------------------------------------------------------------
1 | import { Select, Option } from '@mui/joy'
2 | import { useColumnSelect } from '../model'
3 |
4 | interface IProps {
5 | tableIndex: number
6 | columnId: string
7 | taskIndex: number
8 | }
9 |
10 | export const ColumnSelect = ({ tableIndex, columnId, taskIndex }: IProps) => {
11 | const { options, onChange } = useColumnSelect({
12 | columnId,
13 | tableIndex,
14 | taskIndex,
15 | })
16 |
17 | return (
18 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/src/features/column/switcher/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ui'
2 | export * from './model'
3 |
--------------------------------------------------------------------------------
/src/features/column/switcher/model/index.ts:
--------------------------------------------------------------------------------
1 | export * from './model'
2 |
--------------------------------------------------------------------------------
/src/features/column/switcher/model/model.ts:
--------------------------------------------------------------------------------
1 | import { combine, createEvent, createStore } from 'effector'
2 | import { COLUMN_ITEM_WIDTH } from 'entities/column'
3 | import { $selectedTable } from 'entities/table'
4 |
5 | export const $listViewportWidth = createStore(0)
6 | export const $swipeIndex = createStore(0)
7 | export const $canSwipeLeft = $swipeIndex.map((x) => x > 0)
8 | export const $canSwipeRight = combine(
9 | $listViewportWidth,
10 | $swipeIndex,
11 | $selectedTable,
12 | (listViewportWidth, swipeIndex, table) => {
13 | if (!table) {
14 | return false
15 | }
16 |
17 | const spacing = 16
18 | const listWidth =
19 | table.columns.length * COLUMN_ITEM_WIDTH +
20 | (table.columns.length - 1) * spacing
21 |
22 | return (
23 | listWidth - (COLUMN_ITEM_WIDTH + spacing) * swipeIndex > listViewportWidth
24 | )
25 | }
26 | )
27 |
28 | export const setListWidthEvent = createEvent()
29 | export const swipeLeftEvent = createEvent()
30 | export const swipeRightEvent = createEvent()
31 |
32 | $listViewportWidth.on(setListWidthEvent, (_, value) => value)
33 | $swipeIndex
34 | .on(swipeLeftEvent, (value) => value - 1)
35 | .on(swipeRightEvent, (value) => value + 1)
36 |
--------------------------------------------------------------------------------
/src/features/column/switcher/ui/index.tsx:
--------------------------------------------------------------------------------
1 | //@ts-ignore
2 | import { UilAngleLeft, UilAngleRight } from '@iconscout/react-unicons'
3 | import { Button, Stack } from '@mui/joy'
4 | import { useUnit } from 'effector-react'
5 | import {
6 | $canSwipeLeft,
7 | $canSwipeRight,
8 | swipeLeftEvent,
9 | swipeRightEvent,
10 | } from '../model'
11 |
12 | export const ColumnSwitcher = () => {
13 | const [canSwipeLeft, canSwipeRight, swipeLeft, swipeRight] = useUnit([
14 | $canSwipeLeft,
15 | $canSwipeRight,
16 | swipeLeftEvent,
17 | swipeRightEvent,
18 | ])
19 |
20 | return (
21 |
22 |
31 |
40 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/src/features/table/create/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ui'
2 | export { createTableDialogModel } from './model'
3 |
--------------------------------------------------------------------------------
/src/features/table/create/model/index.ts:
--------------------------------------------------------------------------------
1 | export * from './model'
2 | export * from './use-table-form'
3 |
--------------------------------------------------------------------------------
/src/features/table/create/model/model.ts:
--------------------------------------------------------------------------------
1 | import { createEvent } from 'effector'
2 | import { ICreateTableEventProps } from '../types'
3 | import { $tables, ITable } from 'entities/table'
4 | import { v4 as uuidv4 } from 'uuid'
5 | import { createDialogApi } from 'shared/lib'
6 |
7 | export const createTableEvent = createEvent()
8 |
9 | $tables.on(createTableEvent, (tables, { title, columnTitles }) => {
10 | const id = uuidv4()
11 |
12 | const newTable: ITable = {
13 | id: id,
14 | title,
15 | columns: columnTitles.map((title) => ({
16 | id: uuidv4(),
17 | title,
18 | tasks: [],
19 | tableId: id,
20 | })),
21 | }
22 |
23 | return [...tables, newTable]
24 | })
25 |
26 | export const createTableDialogModel = createDialogApi()
27 |
--------------------------------------------------------------------------------
/src/features/table/create/model/schema.ts:
--------------------------------------------------------------------------------
1 | import { array, object, string } from 'yup'
2 |
3 | export const createTableFormSchema = object().shape({
4 | title: string().required().min(5, 'Min length is 5'),
5 | columns: array().of(
6 | object().shape({
7 | title: string().required().min(3, 'Min length is 3'),
8 | })
9 | ),
10 | })
11 |
--------------------------------------------------------------------------------
/src/features/table/create/model/use-table-form.ts:
--------------------------------------------------------------------------------
1 | import { SubmitHandler, useForm } from 'react-hook-form'
2 | import { ICreateTableFormState } from '../types'
3 | import { createTableEvent } from './model'
4 | import { useUnit } from 'effector-react'
5 | import { yupResolver } from '@hookform/resolvers/yup'
6 | import { createTableFormSchema } from './schema'
7 |
8 | interface IProps {
9 | onCreate: VoidFunction
10 | }
11 |
12 | export const useCreateTableForm = ({ onCreate }: IProps) => {
13 | const { control, handleSubmit, reset } = useForm({
14 | defaultValues: {
15 | title: '',
16 | columns: [{ title: 'Todo' }, { title: 'Doing' }, { title: 'Done' }],
17 | },
18 | resolver: yupResolver(createTableFormSchema as any),
19 | })
20 |
21 | const [createTable] = useUnit([createTableEvent])
22 |
23 | const onSubmit: SubmitHandler = (data) => {
24 | createTable({
25 | title: data.title,
26 | columnTitles: data.columns.map((x) => x.title),
27 | })
28 | reset()
29 | onCreate()
30 | }
31 |
32 | return {
33 | control,
34 | onSubmit: handleSubmit(onSubmit),
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/features/table/create/types.ts:
--------------------------------------------------------------------------------
1 | export interface ICreateTableEventProps {
2 | title: string
3 | columnTitles: string[]
4 | }
5 |
6 | export interface ICreateTableFormState {
7 | title: string
8 | columns: {
9 | id: number
10 | title: string
11 | }[]
12 | }
13 |
--------------------------------------------------------------------------------
/src/features/table/create/ui/column-field/index.tsx:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import { UilTrashAlt } from '@iconscout/react-unicons'
3 | import { FormControl, FormHelperText, IconButton, Input, Stack } from '@mui/joy'
4 | import { Control, Controller } from 'react-hook-form'
5 |
6 | import { ICreateTableFormState } from '../../types'
7 |
8 | interface IProps {
9 | index: number
10 | control: Control
11 | remove: (index: number) => void
12 | }
13 |
14 | export const ColumnField = ({ control, index, remove }: IProps) => {
15 | return (
16 |
17 | (
21 | theme.spacing(0.8) }}
24 | >
25 |
26 | {error && {error?.message}}
27 |
28 | )}
29 | />
30 | remove(index)}>
31 |
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/src/features/table/create/ui/column-form/index.tsx:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import { UilPlus } from '@iconscout/react-unicons'
3 | import { IconButton, Input, Stack } from '@mui/joy'
4 | import { useState } from 'react'
5 |
6 | interface IProps {
7 | append: any
8 | }
9 |
10 | export const ColumnForm = ({ append }: IProps) => {
11 | const [value, setValue] = useState('')
12 |
13 | const handleOnClick = () => {
14 | append({ title: value })
15 | setValue('')
16 | }
17 |
18 | return (
19 |
20 | theme.spacing(1) }}
24 | value={value}
25 | onChange={(event) => setValue(event.currentTarget.value)}
26 | />
27 |
28 |
29 |
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/src/features/table/create/ui/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | DialogTitle,
4 | Modal,
5 | ModalClose,
6 | ModalDialog,
7 | ModalOverflow,
8 | Stack,
9 | Typography,
10 | } from '@mui/joy'
11 | import { useFieldArray } from 'react-hook-form'
12 |
13 | import { ColumnField } from './column-field'
14 | import { ColumnForm } from './column-form'
15 | import { TitleField } from './title-field'
16 |
17 | import { useSnackbar } from 'shared/ui'
18 | import { useCreateTableForm } from '../model/use-table-form'
19 | import { createTableDialogModel } from '../model'
20 |
21 | import { useDialogApi } from 'shared/lib'
22 |
23 | const Content = () => {
24 | const { showSnackbar } = useSnackbar()
25 |
26 | const { open, toggleOpen } = useDialogApi(createTableDialogModel)
27 |
28 | const { control, onSubmit } = useCreateTableForm({
29 | onCreate: () => {
30 | showSnackbar({
31 | message: 'Table created successfully',
32 | type: 'success',
33 | })
34 | toggleOpen()
35 | },
36 | })
37 |
38 | const { fields, append, remove } = useFieldArray({
39 | control,
40 | name: 'columns' as never,
41 | })
42 |
43 | if (!open) {
44 | return
45 | }
46 |
47 | return (
48 |
49 |
50 |
51 |
80 |
81 |
82 |
83 | )
84 | }
85 |
86 | export const CreateTableDialog = () => {
87 | const { open } = useDialogApi(createTableDialogModel)
88 |
89 | return open ? : null
90 | }
91 |
--------------------------------------------------------------------------------
/src/features/table/create/ui/title-field/index.tsx:
--------------------------------------------------------------------------------
1 | import { FormControl, FormHelperText, FormLabel, Input } from '@mui/joy'
2 | import { Control, Controller } from 'react-hook-form'
3 |
4 | import { ICreateTableFormState } from '../../types'
5 |
6 | interface IProps {
7 | control: Control
8 | }
9 |
10 | export const TitleField = ({ control }: IProps) => {
11 | return (
12 | (
16 |
17 | Table name
18 |
24 | {error && {error?.message}}
25 |
26 | )}
27 | />
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/src/features/table/delete/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ui'
2 | export { deleteTableDialogModel } from './model'
3 |
--------------------------------------------------------------------------------
/src/features/table/delete/model/index.ts:
--------------------------------------------------------------------------------
1 | export * from './model'
2 | export * from './use-delete-table'
3 |
--------------------------------------------------------------------------------
/src/features/table/delete/model/model.ts:
--------------------------------------------------------------------------------
1 | import { createEvent } from 'effector'
2 | import { $tables } from 'entities/table'
3 | import { createDialogApi } from 'shared/lib'
4 |
5 | export const deleteTableEvent = createEvent()
6 |
7 | $tables.on(deleteTableEvent, (tables, tableId) =>
8 | tables.filter((x) => x.id !== tableId)
9 | )
10 |
11 | export const deleteTableDialogModel = createDialogApi()
12 |
--------------------------------------------------------------------------------
/src/features/table/delete/model/use-delete-table.ts:
--------------------------------------------------------------------------------
1 | import { useUnit } from 'effector-react'
2 | import { $selectedTable } from 'entities/table'
3 | import { deleteTableDialogModel, deleteTableEvent } from './model'
4 | import { useDialogApi } from 'shared/lib'
5 |
6 | export const useDeleteTable = () => {
7 | const [table, deleteTable] = useUnit([$selectedTable, deleteTableEvent])
8 |
9 | const { open, toggleOpen } = useDialogApi(deleteTableDialogModel)
10 |
11 | const handleDeleteTable = () => {
12 | if (!table) {
13 | return
14 | }
15 |
16 | deleteTable(table.id)
17 | toggleOpen()
18 | }
19 |
20 | return {
21 | open,
22 | toggleOpen,
23 | deleteTable: handleDeleteTable,
24 | table: table!,
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/features/table/delete/ui/index.tsx:
--------------------------------------------------------------------------------
1 | //@ts-ignore
2 | import { UilExclamationTriangle } from '@iconscout/react-unicons'
3 | import {
4 | Button,
5 | DialogActions,
6 | DialogContent,
7 | DialogTitle,
8 | Divider,
9 | Modal,
10 | ModalDialog,
11 | } from '@mui/joy'
12 | import { useDeleteTable } from '../model'
13 |
14 | export const DeleteTableDialog = () => {
15 | const { open, toggleOpen, table, deleteTable } = useDeleteTable()
16 |
17 | if (!open) {
18 | return
19 | }
20 |
21 | return (
22 |
23 |
24 |
25 |
26 | Confirmation
27 |
28 |
29 |
30 | Are you sure you want to delete the "{table.title}" table?
31 | This action will remove all columns and tasks and cannot be reversed.
32 |
33 |
34 |
37 |
40 |
41 |
42 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/src/features/table/index.ts:
--------------------------------------------------------------------------------
1 | export * from './set-table-index'
2 | export * from './create'
3 | export * from './delete'
4 | export * from './update'
5 |
--------------------------------------------------------------------------------
/src/features/table/set-table-index/index.ts:
--------------------------------------------------------------------------------
1 | import { createEvent } from 'effector'
2 | import { $selectedTableIndex } from 'entities/table'
3 |
4 | export const setSelectedTableIndexEvent = createEvent()
5 |
6 | $selectedTableIndex.on(setSelectedTableIndexEvent, (_, value) => value)
7 |
--------------------------------------------------------------------------------
/src/features/table/update/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ui'
2 | export { updateTableDialogModel } from './model'
3 |
--------------------------------------------------------------------------------
/src/features/table/update/model/index.ts:
--------------------------------------------------------------------------------
1 | export * from './model'
2 | export * from './use-table-form'
3 |
--------------------------------------------------------------------------------
/src/features/table/update/model/model.ts:
--------------------------------------------------------------------------------
1 | import { createEvent } from 'effector'
2 | import { IUpdateTableEventProps } from '../types'
3 | import { $tables } from 'entities/table'
4 | import { createDialogApi } from 'shared/lib'
5 |
6 | export const updateTableEvent = createEvent()
7 |
8 | $tables.on(updateTableEvent, (tables, { title, columns, tableIndex }) => {
9 | const table = {
10 | ...tables[tableIndex],
11 | title,
12 | columns: columns
13 | .map((x) => {
14 | if (x.isNewColumn) {
15 | return {
16 | id: x.id,
17 | title: x.title,
18 | tasks: [],
19 | tableId: tables[tableIndex].id,
20 | }
21 | }
22 |
23 | const column = tables[tableIndex].columns.find((y) => y.id === x.id)
24 |
25 | return column ? { ...column, title: x.title } : null
26 | })
27 | .filter(Boolean) as any,
28 | }
29 |
30 | tables[tableIndex] = table
31 |
32 | return [...tables]
33 | })
34 |
35 | export const updateTableDialogModel = createDialogApi()
36 |
--------------------------------------------------------------------------------
/src/features/table/update/model/schema.ts:
--------------------------------------------------------------------------------
1 | import { array, object, string } from 'yup'
2 |
3 | export const createTableFormSchema = object().shape({
4 | title: string().required().min(5, 'Min length is 5'),
5 | columns: array().of(
6 | object().shape({
7 | title: string().required().min(3, 'Min length is 3'),
8 | })
9 | ),
10 | })
11 |
--------------------------------------------------------------------------------
/src/features/table/update/model/use-table-form.ts:
--------------------------------------------------------------------------------
1 | import { SubmitHandler, useForm } from 'react-hook-form'
2 | import { IUpdateTableEventProps, IUpdateTableFormState } from '../types'
3 |
4 | import { useUnit } from 'effector-react'
5 | import { yupResolver } from '@hookform/resolvers/yup'
6 | import { createTableFormSchema } from './schema'
7 | import { updateTableEvent } from './model'
8 | import { $selectedTable, $selectedTableIndex } from 'entities/table'
9 |
10 | interface IProps {
11 | onCreate: VoidFunction
12 | }
13 |
14 | export const useUpdateTableForm = ({ onCreate }: IProps) => {
15 | const [updateTable, table, tableIndex] = useUnit([
16 | updateTableEvent,
17 | $selectedTable,
18 | $selectedTableIndex,
19 | ])
20 |
21 | const { control, handleSubmit, reset } = useForm({
22 | defaultValues: {
23 | title: table?.title!,
24 | columns: table?.columns!.map((x) => ({
25 | id: x.id,
26 | title: x.title,
27 | })),
28 | },
29 | resolver: yupResolver(createTableFormSchema as any),
30 | })
31 |
32 | const onSubmit: SubmitHandler = (data) => {
33 | updateTable({
34 | title: data.title,
35 | columns: data.columns,
36 | tableIndex,
37 | })
38 | reset()
39 | onCreate()
40 | }
41 |
42 | return {
43 | control,
44 | onSubmit: handleSubmit(onSubmit),
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/features/table/update/types.ts:
--------------------------------------------------------------------------------
1 | export interface IUpdateTableEventProps {
2 | title: string
3 | columns: {
4 | id: string
5 | title: string
6 | isNewColumn?: boolean
7 | }[]
8 | tableIndex: number
9 | }
10 |
11 | export interface IUpdateTableFormState {
12 | title: string
13 | columns: {
14 | id: string
15 | title: string
16 | isNewColumn?: boolean
17 | }[]
18 | }
19 |
--------------------------------------------------------------------------------
/src/features/table/update/ui/column-field/index.tsx:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import { UilTrashAlt } from '@iconscout/react-unicons'
3 | import { FormControl, FormHelperText, IconButton, Input, Stack } from '@mui/joy'
4 | import { Control, Controller } from 'react-hook-form'
5 | import { IUpdateTableFormState } from '../../types'
6 |
7 | interface IProps {
8 | index: number
9 | control: Control
10 | remove: (index: number) => void
11 | }
12 |
13 | export const ColumnField = ({ control, index, remove }: IProps) => {
14 | return (
15 |
16 | (
20 | theme.spacing(0.8) }}
23 | >
24 |
25 | {error && {error?.message}}
26 |
27 | )}
28 | />
29 | remove(index)}>
30 |
31 |
32 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/src/features/table/update/ui/column-form/index.tsx:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import { UilPlus } from '@iconscout/react-unicons'
3 | import { IconButton, Input, Stack } from '@mui/joy'
4 | import { useState } from 'react'
5 | import { v4 as uuid } from 'uuid'
6 |
7 | interface IProps {
8 | append: any
9 | }
10 |
11 | export const ColumnForm = ({ append }: IProps) => {
12 | const [value, setValue] = useState('')
13 |
14 | const handleOnClick = () => {
15 | append({
16 | id: uuid(),
17 | title: value,
18 | isNewColumn: true,
19 | })
20 | setValue('')
21 | }
22 |
23 | return (
24 |
25 | theme.spacing(1) }}
29 | value={value}
30 | onChange={(event) => setValue(event.currentTarget.value)}
31 | />
32 |
33 |
34 |
35 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/src/features/table/update/ui/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | DialogTitle,
4 | Modal,
5 | ModalClose,
6 | ModalDialog,
7 | ModalOverflow,
8 | Stack,
9 | Typography,
10 | } from '@mui/joy'
11 | import { useFieldArray } from 'react-hook-form'
12 |
13 | import { ColumnField } from './column-field'
14 | import { ColumnForm } from './column-form'
15 | import { TitleField } from './title-field'
16 |
17 | import { useSnackbar } from 'shared/ui'
18 |
19 | import { useDialogApi } from 'shared/lib'
20 | import { updateTableDialogModel, useUpdateTableForm } from '../model'
21 |
22 | const Content = () => {
23 | const { showSnackbar } = useSnackbar()
24 |
25 | const { open, toggleOpen } = useDialogApi(updateTableDialogModel)
26 |
27 | const { control, onSubmit } = useUpdateTableForm({
28 | onCreate: () => {
29 | showSnackbar({
30 | message: 'Table updated successfully',
31 | type: 'success',
32 | })
33 | toggleOpen()
34 | },
35 | })
36 |
37 | const { fields, append, remove } = useFieldArray({
38 | control,
39 | name: 'columns' as never,
40 | })
41 |
42 | if (!open) {
43 | return
44 | }
45 |
46 | return (
47 |
48 |
49 |
50 |
79 |
80 |
81 |
82 | )
83 | }
84 |
85 | export const UpdateTableDialog = () => {
86 | const { open, toggleOpen } = useDialogApi(updateTableDialogModel)
87 |
88 | return open ? : null
89 | }
90 |
--------------------------------------------------------------------------------
/src/features/table/update/ui/title-field/index.tsx:
--------------------------------------------------------------------------------
1 | import { FormControl, FormHelperText, FormLabel, Input } from '@mui/joy'
2 | import { Control, Controller } from 'react-hook-form'
3 | import { IUpdateTableFormState } from '../../types'
4 |
5 | interface IProps {
6 | control: Control
7 | }
8 |
9 | export const TitleField = ({ control }: IProps) => {
10 | return (
11 | (
15 |
16 | Table name
17 |
23 | {error && {error?.message}}
24 |
25 | )}
26 | />
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/src/features/task/create/index.ts:
--------------------------------------------------------------------------------
1 | export { createTaskDialogModel } from './model'
2 | export * from './ui'
3 |
--------------------------------------------------------------------------------
/src/features/task/create/model/index.ts:
--------------------------------------------------------------------------------
1 | export * from './model'
2 | export * from './use-task-form'
3 |
--------------------------------------------------------------------------------
/src/features/task/create/model/model.ts:
--------------------------------------------------------------------------------
1 | import { createEvent } from 'effector'
2 | import { createDialogApi } from 'shared/lib'
3 | import { ICreateTaskEventProps } from '../types'
4 | import { $tables } from 'entities/table'
5 | import { ITask } from 'entities/task'
6 | import { v4 as uuid } from 'uuid'
7 |
8 | export const createTaskEvent = createEvent()
9 |
10 | $tables.on(
11 | createTaskEvent,
12 | (
13 | tables,
14 | { title, description, todos, columnId, tableIndex, columnIndex }
15 | ) => {
16 | const newTask: ITask = {
17 | id: uuid(),
18 | title,
19 | description,
20 | todos: todos.map((title) => ({
21 | id: uuid(),
22 | title,
23 | isDone: false,
24 | })),
25 | columnId,
26 | }
27 |
28 | const tasks = tables[tableIndex].columns[columnIndex].tasks
29 |
30 | tables[tableIndex].columns[columnIndex].tasks = [...tasks, newTask]
31 |
32 | tables[tableIndex] = { ...tables[tableIndex] }
33 |
34 | return [...tables]
35 | }
36 | )
37 |
38 | export const createTaskDialogModel = createDialogApi()
39 |
--------------------------------------------------------------------------------
/src/features/task/create/model/schema.ts:
--------------------------------------------------------------------------------
1 | import { array, object, string } from 'yup'
2 |
3 | export const createTaskDialogSchema = object().shape({
4 | title: string().required('Title is required').min(5, 'Min length is 5'),
5 | description: string()
6 | .required('Description is required')
7 | .min(5, 'Min length is 5'),
8 | todos: array().of(
9 | object().shape({
10 | title: string().required('Title is required').min(3, 'Min length is 3'),
11 | })
12 | ),
13 | columnId: string().required('Column is required'),
14 | })
15 |
--------------------------------------------------------------------------------
/src/features/task/create/model/use-task-form.ts:
--------------------------------------------------------------------------------
1 | import { SubmitHandler, useForm } from 'react-hook-form'
2 | import { ICreateTaskFormState } from '../types'
3 | import { useUnit } from 'effector-react'
4 | import { $selectedTable, $selectedTableIndex } from 'entities/table'
5 | import { createTaskEvent } from './model'
6 | import { yupResolver } from '@hookform/resolvers/yup'
7 | import { createTaskDialogSchema } from './schema'
8 |
9 | interface IProps {
10 | onCreate: VoidFunction
11 | }
12 |
13 | export const useCreateTaskForm = ({ onCreate }: IProps) => {
14 | const { control, handleSubmit, reset } = useForm({
15 | defaultValues: {
16 | title: '',
17 | description: '',
18 | todos: [],
19 | columnId: '',
20 | },
21 | resolver: yupResolver(createTaskDialogSchema as any),
22 | })
23 |
24 | const [table, tableIndex, createTask] = useUnit([
25 | $selectedTable,
26 | $selectedTableIndex,
27 | createTaskEvent,
28 | ])
29 |
30 | const onSubmit: SubmitHandler = (data) => {
31 | createTask({
32 | title: data.title,
33 | description: data.description,
34 | columnId: data.columnId,
35 | todos: data.todos.map((x) => x.title),
36 | tableIndex: tableIndex,
37 | columnIndex:
38 | table?.columns.findIndex((x) => x.id === data.columnId) ?? -1,
39 | })
40 | reset()
41 | onCreate()
42 | }
43 |
44 | return {
45 | control,
46 | onSubmit: handleSubmit(onSubmit),
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/features/task/create/types.ts:
--------------------------------------------------------------------------------
1 | export interface ICreateTaskEventProps {
2 | title: string
3 | description: string
4 | todos: string[]
5 | columnId: string
6 | tableIndex: number
7 | columnIndex: number
8 | }
9 |
10 | export interface ICreateTaskFormState {
11 | title: string
12 | description: string
13 | todos: {
14 | title: string
15 | }[]
16 | columnId: string
17 | }
18 |
--------------------------------------------------------------------------------
/src/features/task/create/ui/column-select/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | FormControl,
3 | FormHelperText,
4 | FormLabel,
5 | Option,
6 | Select,
7 | } from '@mui/joy'
8 | import { useUnit } from 'effector-react'
9 | import { $selectedTable } from 'entities/table'
10 | import { Control, Controller } from 'react-hook-form'
11 |
12 | interface IProps {
13 | control: Control
14 | }
15 |
16 | export const ColumnSelect = ({ control }: IProps) => {
17 | const table = useUnit($selectedTable)
18 |
19 | if (!table) {
20 | return null
21 | }
22 |
23 | return (
24 | {
28 | const handleOnChange = (e: any, value: any) => {
29 | onChange(value)
30 | }
31 |
32 | return (
33 |
34 | Column
35 |
48 | {error && {error.message}}
49 |
50 | )
51 | }}
52 | />
53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/src/features/task/create/ui/description-field/index.tsx:
--------------------------------------------------------------------------------
1 | import { FormControl, FormHelperText, FormLabel, Textarea } from '@mui/joy'
2 | import { Control, Controller } from 'react-hook-form'
3 |
4 | interface IProps {
5 | control: Control
6 | }
7 |
8 | export const DescriptionField = ({ control }: IProps) => {
9 | return (
10 | (
14 |
15 | Description
16 |
23 | {error && {error?.message}}
24 |
25 | )}
26 | />
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/src/features/task/create/ui/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Modal, ModalDialog, ModalOverflow, Stack } from '@mui/joy'
2 |
3 | import { useFieldArray } from 'react-hook-form'
4 |
5 | import { ColumnSelect } from './column-select'
6 | import { DescriptionField } from './description-field'
7 | import { TitleField } from './title-field'
8 | import { TodoField } from './todo-field'
9 | import { TodoForm } from './todo-form'
10 | import { createTaskDialogModel, useCreateTaskForm } from '../model'
11 | import { useSnackbar } from 'shared/ui'
12 | import { useDialogApi } from 'shared/lib'
13 |
14 | const Content = () => {
15 | const { open, toggleOpen } = useDialogApi(createTaskDialogModel)
16 |
17 | const { showSnackbar } = useSnackbar()
18 |
19 | const { control, onSubmit } = useCreateTaskForm({
20 | onCreate: () => {
21 | showSnackbar({
22 | type: 'success',
23 | message: 'Task created successfully',
24 | })
25 | toggleOpen()
26 | },
27 | })
28 |
29 | const { fields, append, remove } = useFieldArray({
30 | control,
31 | name: 'todos' as never,
32 | })
33 |
34 | return (
35 |
36 |
37 |
38 |
57 |
58 |
59 |
60 | )
61 | }
62 |
63 | export const CreateTaskDialog = () => {
64 | const { open } = useDialogApi(createTaskDialogModel)
65 |
66 | return open ? : null
67 | }
68 |
--------------------------------------------------------------------------------
/src/features/task/create/ui/title-field/index.tsx:
--------------------------------------------------------------------------------
1 | import { FormControl, FormHelperText, FormLabel, Input } from '@mui/joy'
2 | import { Control, Controller } from 'react-hook-form'
3 |
4 | interface IProps {
5 | control: Control
6 | }
7 |
8 | export const TitleField = ({ control }: IProps) => {
9 | return (
10 | (
14 |
15 | Task name
16 |
22 | {error && {error?.message}}
23 |
24 | )}
25 | />
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/src/features/task/create/ui/todo-field/index.tsx:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import { UilTrashAlt } from '@iconscout/react-unicons'
3 | import { FormControl, FormHelperText, IconButton, Input, Stack } from '@mui/joy'
4 | import { Control, Controller } from 'react-hook-form'
5 |
6 | interface IProps {
7 | index: number
8 | control: Control
9 | remove: (index: number) => void
10 | }
11 |
12 | export const TodoField = ({ control, index, remove }: IProps) => {
13 | return (
14 |
15 | (
19 | theme.spacing(0.8) }}
22 | >
23 |
24 | {error && {error?.message}}
25 |
26 | )}
27 | />
28 | remove(index)}>
29 |
30 |
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/src/features/task/create/ui/todo-form/index.tsx:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import { UilPlus } from '@iconscout/react-unicons'
3 | import { IconButton, Input, Stack } from '@mui/joy'
4 | import { useState } from 'react'
5 |
6 | interface IProps {
7 | append: any
8 | }
9 |
10 | export const TodoForm = ({ append }: IProps) => {
11 | const [value, setValue] = useState('')
12 |
13 | const handleOnClick = () => {
14 | append({ title: value })
15 | setValue('')
16 | }
17 |
18 | return (
19 |
20 | theme.spacing(1) }}
24 | value={value}
25 | onChange={(event) => setValue(event.currentTarget.value)}
26 | />
27 |
28 |
29 |
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/src/features/task/delete/index.ts:
--------------------------------------------------------------------------------
1 | export { setDeleteTaskOptionsEvent } from './model'
2 | export * from './ui'
3 |
--------------------------------------------------------------------------------
/src/features/task/delete/model/index.ts:
--------------------------------------------------------------------------------
1 | export * from './model'
2 | export * from './use-task-delete'
3 | export * from './use-delete-dialog'
4 |
--------------------------------------------------------------------------------
/src/features/task/delete/model/model.ts:
--------------------------------------------------------------------------------
1 | import { createEvent, createStore } from 'effector'
2 | import { IDeleteTaskEventProps } from '../types'
3 | import { $tables } from 'entities/table'
4 |
5 | import { ITask } from 'entities/task'
6 |
7 | export const deleteTaskEvent = createEvent()
8 |
9 | $tables.on(
10 | deleteTaskEvent,
11 | (tables, { tableIndex, columnIndex, taskIndex }) => {
12 | tables[tableIndex].columns[columnIndex].tasks = tables[tableIndex].columns[
13 | columnIndex
14 | ].tasks.filter((_, i) => i !== taskIndex)
15 |
16 | tables[tableIndex] = { ...tables[tableIndex] }
17 |
18 | return [...tables]
19 | }
20 | )
21 |
22 | export const $deleteTaskOptions = createStore<{
23 | tableIndex: number
24 | columnIndex: number
25 | taskIndex: number
26 | } | null>(null)
27 |
28 | export const setDeleteTaskOptionsEvent = createEvent<{
29 | tableIndex: number
30 | columnIndex: number
31 | taskIndex: number
32 | } | null>()
33 |
34 | $deleteTaskOptions.on(setDeleteTaskOptionsEvent, (_, value) => value)
35 |
--------------------------------------------------------------------------------
/src/features/task/delete/model/use-delete-dialog.ts:
--------------------------------------------------------------------------------
1 | import { useUnit } from 'effector-react'
2 | import { $deleteTaskOptions, setDeleteTaskOptionsEvent } from './model'
3 |
4 | export const useDeleteTaskDialog = () => {
5 | const [deleteTaskOptions, setDeleteTaskOptions] = useUnit([
6 | $deleteTaskOptions,
7 | setDeleteTaskOptionsEvent,
8 | ])
9 |
10 | const handleOnClose = () => {
11 | setDeleteTaskOptions(null)
12 | }
13 |
14 | return {
15 | open: Boolean(deleteTaskOptions),
16 | onClose: handleOnClose,
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/features/task/delete/model/use-task-delete.ts:
--------------------------------------------------------------------------------
1 | import { useUnit } from 'effector-react'
2 |
3 | import {
4 | $deleteTaskOptions,
5 | deleteTaskEvent,
6 | setDeleteTaskOptionsEvent,
7 | } from './model'
8 | import { useTask } from 'entities/task'
9 | import { useDeleteTaskDialog } from './use-delete-dialog'
10 |
11 | export const useTaskDelete = () => {
12 | const [deleteTaskOptions, deleteTask] = useUnit([
13 | $deleteTaskOptions,
14 | deleteTaskEvent,
15 | ])
16 |
17 | const { task } = useTask(deleteTaskOptions!)
18 |
19 | const { onClose } = useDeleteTaskDialog()
20 |
21 | const handleDeleteTask = () => {
22 | deleteTask({
23 | tableIndex: deleteTaskOptions!.tableIndex,
24 | columnIndex: deleteTaskOptions!.columnIndex,
25 | taskIndex: deleteTaskOptions!.taskIndex,
26 | })
27 | onClose()
28 | }
29 |
30 | return {
31 | deleteTable: handleDeleteTask,
32 | task,
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/features/task/delete/types.ts:
--------------------------------------------------------------------------------
1 | export interface IDeleteTaskEventProps {
2 | tableIndex: number
3 | columnIndex: number
4 | taskIndex: number
5 | }
6 |
--------------------------------------------------------------------------------
/src/features/task/delete/ui/index.tsx:
--------------------------------------------------------------------------------
1 | //@ts-ignore
2 | import { UilExclamationTriangle } from '@iconscout/react-unicons'
3 | import {
4 | Button,
5 | DialogActions,
6 | DialogContent,
7 | DialogTitle,
8 | Divider,
9 | Modal,
10 | ModalDialog,
11 | } from '@mui/joy'
12 | import { useDeleteTaskDialog, useTaskDelete } from '../model'
13 |
14 | const Content = () => {
15 | const { task, deleteTable } = useTaskDelete()
16 |
17 | const { onClose } = useDeleteTaskDialog()
18 |
19 | return (
20 |
21 |
22 |
23 |
24 | Confirmation
25 |
26 |
27 |
28 | Are you sure you want to delete the "{task?.title}" task?
29 | This action will remove all todos and cannot be reversed.
30 |
31 |
32 |
35 |
38 |
39 |
40 |
41 | )
42 | }
43 |
44 | export const DeleteTaskDialog = () => {
45 | const { open } = useDeleteTaskDialog()
46 |
47 | return open ? : null
48 | }
49 |
--------------------------------------------------------------------------------
/src/features/task/index.ts:
--------------------------------------------------------------------------------
1 | export * from './create'
2 | export * from './delete'
3 | export * from './update'
4 |
--------------------------------------------------------------------------------
/src/features/task/update/index.ts:
--------------------------------------------------------------------------------
1 | export { setUpdateTaskOptionsEvent } from './model'
2 | export * from './ui'
3 |
--------------------------------------------------------------------------------
/src/features/task/update/model/index.ts:
--------------------------------------------------------------------------------
1 | export * from './use-task-form'
2 | export * from './model'
3 | export * from './use-update-dialog'
4 |
--------------------------------------------------------------------------------
/src/features/task/update/model/model.ts:
--------------------------------------------------------------------------------
1 | import { createEvent, createStore } from 'effector'
2 |
3 | import { IUpdateTaskEventProps } from '../types'
4 | import { $tables } from 'entities/table'
5 | import { v4 as uuid } from 'uuid'
6 | import { $openedTaskOptions } from 'entities/task'
7 |
8 | export const updateTaskEvent = createEvent()
9 |
10 | $tables.on(
11 | updateTaskEvent,
12 | (
13 | tables,
14 | { title, description, todos, columnId, tableIndex, columnIndex, taskIndex }
15 | ) => {
16 | const taskTodos =
17 | tables[tableIndex].columns[columnIndex].tasks[taskIndex].todos
18 |
19 | const newColumnIndex = tables[tableIndex].columns.findIndex(
20 | (x) => x.id === columnId
21 | )
22 | const newTaskIndex = tables[tableIndex].columns[
23 | newColumnIndex
24 | ].tasks.findIndex(
25 | (x) =>
26 | x.id !== tables[tableIndex].columns[columnIndex].tasks[taskIndex].id
27 | )
28 |
29 | const task = {
30 | ...tables[tableIndex].columns[columnIndex].tasks[taskIndex],
31 | title,
32 | description,
33 | todos: todos
34 | .map((x) => {
35 | if (x.isNewTodo) {
36 | return {
37 | id: uuid(),
38 | title: x.title,
39 | isDone: false,
40 | }
41 | }
42 |
43 | const todo = taskTodos.find((y) => y.id === x.id)
44 |
45 | return todo ? { ...todo, title: x.title } : null
46 | })
47 | .filter(Boolean) as any,
48 | columnId:
49 | newTaskIndex > -1
50 | ? tables[tableIndex].columns[newColumnIndex].id
51 | : columnId,
52 | }
53 |
54 | if (newTaskIndex === -1) {
55 | tables[tableIndex].columns[columnIndex].tasks = tables[
56 | tableIndex
57 | ].columns[columnIndex].tasks.filter((x, i) => i !== taskIndex)
58 |
59 | tables[tableIndex].columns[newColumnIndex].tasks = [
60 | ...tables[tableIndex].columns[newColumnIndex].tasks,
61 | task,
62 | ]
63 | } else {
64 | tables[tableIndex].columns[columnIndex].tasks[taskIndex] = task
65 | }
66 |
67 | tables[tableIndex].columns[columnIndex].tasks = [
68 | ...tables[tableIndex].columns[columnIndex].tasks,
69 | ]
70 |
71 | tables[tableIndex] = { ...tables[tableIndex] }
72 |
73 | return [...tables]
74 | }
75 | )
76 |
77 | $openedTaskOptions.on(updateTaskEvent, (state, payload) => {
78 | if (!state) {
79 | return state
80 | }
81 |
82 | const tables = $tables.getState()
83 | const newColumnIndex = tables[payload.tableIndex].columns.findIndex(
84 | (x) => x.id === payload.columnId
85 | )
86 |
87 | if (payload.columnIndex !== newColumnIndex) {
88 | return {
89 | ...state,
90 | columnIndex: newColumnIndex,
91 | taskIndex:
92 | tables[payload.tableIndex].columns[newColumnIndex].tasks.length - 1,
93 | }
94 | }
95 |
96 | return state
97 | })
98 |
99 | export const $updateTaskOptions = createStore<{
100 | tableIndex: number
101 | columnIndex: number
102 | taskIndex: number
103 | } | null>(null)
104 |
105 | export const setUpdateTaskOptionsEvent = createEvent<{
106 | tableIndex: number
107 | columnIndex: number
108 | taskIndex: number
109 | } | null>()
110 |
111 | $updateTaskOptions.on(setUpdateTaskOptionsEvent, (_, value) => value)
112 |
--------------------------------------------------------------------------------
/src/features/task/update/model/schema.ts:
--------------------------------------------------------------------------------
1 | import { array, object, string } from 'yup'
2 |
3 | export const updateTaskDialogSchema = object().shape({
4 | title: string().required('Title is required').min(5, 'Min length is 5'),
5 | description: string()
6 | .required('Description is required')
7 | .min(5, 'Min length is 5'),
8 | todos: array().of(
9 | object().shape({
10 | title: string().required('Title is required').min(3, 'Min length is 3'),
11 | })
12 | ),
13 | columnId: string().required('Column is required'),
14 | })
15 |
--------------------------------------------------------------------------------
/src/features/task/update/model/use-task-form.ts:
--------------------------------------------------------------------------------
1 | import { SubmitHandler, useForm } from 'react-hook-form'
2 | import { IUpdateTaskFormState } from '../types'
3 | import { useUnit } from 'effector-react'
4 |
5 | import { yupResolver } from '@hookform/resolvers/yup'
6 | import { updateTaskDialogSchema } from './schema'
7 | import { $updateTaskOptions, updateTaskEvent } from './model'
8 | import { useTask } from 'entities/task'
9 |
10 | interface IProps {
11 | onCreate: VoidFunction
12 | }
13 |
14 | export const useUpdateTaskForm = ({ onCreate }: IProps) => {
15 | const [updateTaskOptions, updateTask] = useUnit([
16 | $updateTaskOptions,
17 | updateTaskEvent,
18 | ])
19 |
20 | const { tableIndex, columnIndex, taskIndex } = updateTaskOptions!
21 |
22 | const { task } = useTask({ tableIndex, columnIndex, taskIndex })
23 |
24 | const { control, handleSubmit, reset } = useForm({
25 | defaultValues: {
26 | title: task.title,
27 | description: task.description,
28 | todos: task.todos.map((x) => ({
29 | id: x.id,
30 | title: x.title,
31 | })),
32 | columnId: task.columnId,
33 | },
34 | resolver: yupResolver(updateTaskDialogSchema as any),
35 | })
36 |
37 | const onSubmit: SubmitHandler = (data) => {
38 | updateTask({
39 | title: data.title,
40 | description: data.description,
41 | columnId: data.columnId,
42 | todos: data.todos,
43 | tableIndex,
44 | columnIndex,
45 | taskIndex,
46 | })
47 | reset()
48 | onCreate()
49 | }
50 |
51 | return {
52 | control,
53 | onSubmit: handleSubmit(onSubmit),
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/features/task/update/model/use-update-dialog.ts:
--------------------------------------------------------------------------------
1 | import { useUnit } from 'effector-react'
2 | import { $updateTaskOptions, setUpdateTaskOptionsEvent } from './model'
3 |
4 | export const useUpdateTaskDialog = () => {
5 | const [updateTaskOptions, setUpdateTaskOptions] = useUnit([
6 | $updateTaskOptions,
7 | setUpdateTaskOptionsEvent,
8 | ])
9 |
10 | const handleOnClose = () => {
11 | setUpdateTaskOptions(null)
12 | }
13 |
14 | return {
15 | open: Boolean(updateTaskOptions),
16 | onClose: handleOnClose,
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/features/task/update/types.ts:
--------------------------------------------------------------------------------
1 | export interface IUpdateTaskEventProps {
2 | title: string
3 | description: string
4 | todos: {
5 | id: string
6 | title: string
7 | isNewTodo?: boolean
8 | }[]
9 | columnId: string
10 | tableIndex: number
11 | columnIndex: number
12 | taskIndex: number
13 | }
14 |
15 | export interface IUpdateTaskFormState {
16 | title: string
17 | description: string
18 | todos: {
19 | id: string
20 | title: string
21 | isNewTodo?: boolean
22 | }[]
23 | columnId: string
24 | }
25 |
--------------------------------------------------------------------------------
/src/features/task/update/ui/column-select/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | FormControl,
3 | FormHelperText,
4 | FormLabel,
5 | Option,
6 | Select,
7 | } from '@mui/joy'
8 | import { useUnit } from 'effector-react'
9 | import { $selectedTable } from 'entities/table'
10 | import { Control, Controller } from 'react-hook-form'
11 |
12 | interface IProps {
13 | control: Control
14 | }
15 |
16 | export const ColumnSelect = ({ control }: IProps) => {
17 | const table = useUnit($selectedTable)
18 |
19 | if (!table) {
20 | return null
21 | }
22 |
23 | return (
24 | {
28 | const handleOnChange = (e: any, value: any) => {
29 | onChange(value)
30 | }
31 |
32 | return (
33 |
34 | Column
35 |
48 | {error && {error.message}}
49 |
50 | )
51 | }}
52 | />
53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/src/features/task/update/ui/description-field/index.tsx:
--------------------------------------------------------------------------------
1 | import { FormControl, FormHelperText, FormLabel, Textarea } from '@mui/joy'
2 | import { Control, Controller } from 'react-hook-form'
3 |
4 | interface IProps {
5 | control: Control
6 | }
7 |
8 | export const DescriptionField = ({ control }: IProps) => {
9 | return (
10 | (
14 |
15 | Description
16 |
23 | {error && {error?.message}}
24 |
25 | )}
26 | />
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/src/features/task/update/ui/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | DialogTitle,
4 | Modal,
5 | ModalClose,
6 | ModalDialog,
7 | ModalOverflow,
8 | Stack,
9 | Typography,
10 | } from '@mui/joy'
11 | import { useFieldArray } from 'react-hook-form'
12 |
13 | import { TitleField } from './title-field'
14 |
15 | import { useSnackbar } from 'shared/ui'
16 |
17 | import { useUpdateTaskDialog, useUpdateTaskForm } from '../model'
18 | import { DescriptionField } from './description-field'
19 | import { TodoForm } from './todo-form'
20 | import { TodoField } from './todo-field'
21 | import { ColumnSelect } from './column-select'
22 |
23 | const Content = () => {
24 | const { showSnackbar } = useSnackbar()
25 |
26 | const { onClose } = useUpdateTaskDialog()
27 |
28 | const { control, onSubmit } = useUpdateTaskForm({
29 | onCreate: () => {
30 | showSnackbar({
31 | message: 'Task updated successfully',
32 | type: 'success',
33 | })
34 | onClose()
35 | },
36 | })
37 |
38 | const { fields, append, remove } = useFieldArray({
39 | control,
40 | name: 'todos' as never,
41 | })
42 |
43 | return (
44 |
45 |
46 |
47 |
78 |
79 |
80 |
81 | )
82 | }
83 |
84 | export const UpdateTaskDialog = () => {
85 | const { open } = useUpdateTaskDialog()
86 |
87 | return open ? : null
88 | }
89 |
--------------------------------------------------------------------------------
/src/features/task/update/ui/title-field/index.tsx:
--------------------------------------------------------------------------------
1 | import { FormControl, FormHelperText, FormLabel, Input } from '@mui/joy'
2 | import { Control, Controller } from 'react-hook-form'
3 | import { IUpdateTaskFormState } from '../../types'
4 |
5 | interface IProps {
6 | control: Control
7 | }
8 |
9 | export const TitleField = ({ control }: IProps) => {
10 | return (
11 | (
15 |
16 | Task name
17 |
23 | {error && {error?.message}}
24 |
25 | )}
26 | />
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/src/features/task/update/ui/todo-field/index.tsx:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import { UilTrashAlt } from '@iconscout/react-unicons'
3 | import { FormControl, FormHelperText, IconButton, Input, Stack } from '@mui/joy'
4 | import { Control, Controller } from 'react-hook-form'
5 |
6 | interface IProps {
7 | index: number
8 | control: Control
9 | remove: (index: number) => void
10 | }
11 |
12 | export const TodoField = ({ control, index, remove }: IProps) => {
13 | return (
14 |
15 | (
19 | theme.spacing(0.8) }}
22 | >
23 |
24 | {error && {error?.message}}
25 |
26 | )}
27 | />
28 | remove(index)}>
29 |
30 |
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/src/features/task/update/ui/todo-form/index.tsx:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import { UilPlus } from '@iconscout/react-unicons'
3 | import { IconButton, Input, Stack } from '@mui/joy'
4 | import { useState } from 'react'
5 | import { v4 as uuid } from 'uuid'
6 |
7 | interface IProps {
8 | append: any
9 | }
10 |
11 | export const TodoForm = ({ append }: IProps) => {
12 | const [value, setValue] = useState('')
13 |
14 | const handleOnClick = () => {
15 | append({
16 | id: uuid(),
17 | title: value,
18 | isNewTodo: true,
19 | })
20 | setValue('')
21 | }
22 |
23 | return (
24 |
25 | theme.spacing(1) }}
29 | value={value}
30 | onChange={(event) => setValue(event.currentTarget.value)}
31 | />
32 |
33 |
34 |
35 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/src/features/theme/index.ts:
--------------------------------------------------------------------------------
1 | export * from './switcher'
2 |
--------------------------------------------------------------------------------
/src/features/theme/switcher/index.tsx:
--------------------------------------------------------------------------------
1 | import { Switch } from '@mui/joy'
2 | import { ReactNode } from 'react'
3 |
4 | import { useThemeSwitcher } from './model'
5 |
6 | interface IProps {
7 | className?: string
8 | startDecorator?: ReactNode
9 | endDecorator?: ReactNode
10 | }
11 |
12 | export const ThemeSwitch = ({
13 | className,
14 | startDecorator,
15 | endDecorator,
16 | }: IProps) => {
17 | const { isChecked, toggleMode } = useThemeSwitcher()
18 |
19 | return (
20 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/src/features/theme/switcher/model.ts:
--------------------------------------------------------------------------------
1 | import { useColorScheme } from '@mui/joy'
2 | import { useEvent } from 'shared/lib'
3 |
4 | export const useThemeSwitcher = () => {
5 | const { mode, setMode } = useColorScheme()
6 |
7 | const isLightMode = mode === 'light'
8 |
9 | const toggleMode = useEvent(() => {
10 | setMode(isLightMode ? 'dark' : 'light')
11 | })
12 |
13 | return {
14 | isChecked: !isLightMode,
15 | toggleMode,
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/features/todo/index.ts:
--------------------------------------------------------------------------------
1 | export * from './toggle'
2 |
--------------------------------------------------------------------------------
/src/features/todo/toggle/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ui'
2 |
--------------------------------------------------------------------------------
/src/features/todo/toggle/model/index.ts:
--------------------------------------------------------------------------------
1 | export * from './use-toggle-todo'
2 |
--------------------------------------------------------------------------------
/src/features/todo/toggle/model/model.ts:
--------------------------------------------------------------------------------
1 | import { createEvent } from 'effector'
2 | import { IToggleTodoEventProps } from '../types'
3 | import { $tables } from 'entities/table'
4 |
5 | export const toggleTodoEvent = createEvent()
6 |
7 | $tables.on(
8 | toggleTodoEvent,
9 | (tables, { tableIndex, taskIndex, columnIndex, todoIndex }) => {
10 | tables[tableIndex].columns[columnIndex].tasks[taskIndex].todos[
11 | todoIndex
12 | ].isDone =
13 | !tables[tableIndex].columns[columnIndex].tasks[taskIndex].todos[todoIndex]
14 | .isDone
15 |
16 | tables[tableIndex] = { ...tables[tableIndex] }
17 |
18 | return [...tables]
19 | }
20 | )
21 |
--------------------------------------------------------------------------------
/src/features/todo/toggle/model/use-toggle-todo.ts:
--------------------------------------------------------------------------------
1 | import { useUnit } from 'effector-react'
2 |
3 | import { toggleTodoEvent } from './model'
4 |
5 | interface IProps {
6 | tableIndex: number
7 | columnIndex: number
8 | taskIndex: number
9 | todoIndex: number
10 | }
11 |
12 | export const useToggleTodo = ({
13 | tableIndex,
14 | columnIndex,
15 | taskIndex,
16 | todoIndex,
17 | }: IProps) => {
18 | const [toggleTodo] = useUnit([toggleTodoEvent])
19 |
20 | const handleToggleTodo = () => {
21 | toggleTodo({
22 | tableIndex,
23 | columnIndex,
24 | taskIndex,
25 | todoIndex,
26 | })
27 | }
28 |
29 | return {
30 | toggleTodo: handleToggleTodo,
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/features/todo/toggle/types.ts:
--------------------------------------------------------------------------------
1 | export interface IToggleTodoEventProps {
2 | tableIndex: number
3 | columnIndex: number
4 | taskIndex: number
5 | todoIndex: number
6 | }
7 |
--------------------------------------------------------------------------------
/src/features/todo/toggle/ui/index.tsx:
--------------------------------------------------------------------------------
1 | import { Checkbox } from '@mui/joy'
2 | import { useToggleTodo } from '../model'
3 | import { ITodo } from 'entities/todo'
4 |
5 | interface IProps {
6 | tableIndex: number
7 | columnIndex: number
8 | taskIndex: number
9 | todoIndex: number
10 | todo: ITodo
11 | className?: string
12 | }
13 |
14 | export const ToggleTodo = ({
15 | tableIndex,
16 | columnIndex,
17 | taskIndex,
18 | todoIndex,
19 | todo,
20 | className,
21 | }: IProps) => {
22 | const { toggleTodo } = useToggleTodo({
23 | tableIndex,
24 | columnIndex,
25 | taskIndex,
26 | todoIndex,
27 | })
28 |
29 | return (
30 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/src/pages/home/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ui'
2 |
--------------------------------------------------------------------------------
/src/pages/home/ui/index.tsx:
--------------------------------------------------------------------------------
1 | import { useUnit } from 'effector-react'
2 | import { WelcomeCard } from './welcome-card'
3 | import { $selectedTable } from 'entities/table'
4 | import { Box } from '@mui/joy'
5 | import { ColumnList } from 'widgets/column'
6 |
7 | export const HomePage = () => {
8 | const [table] = useUnit([$selectedTable])
9 |
10 | return (
11 | <>
12 | Kanban task management
13 | {table ? (
14 |
15 |
16 |
17 | ) : (
18 |
19 | )}
20 | >
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/src/pages/home/ui/welcome-card/get-started-button/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@mui/joy'
2 |
3 | import { useGetStartedButton } from './model'
4 |
5 | export const GetStartedButton = () => {
6 | const { onClick } = useGetStartedButton()
7 |
8 | return
9 | }
10 |
--------------------------------------------------------------------------------
/src/pages/home/ui/welcome-card/get-started-button/model.ts:
--------------------------------------------------------------------------------
1 | import { useUnit } from 'effector-react'
2 |
3 | import { $tables } from 'entities/table'
4 | import {
5 | createTableDialogModel,
6 | setSelectedTableIndexEvent,
7 | } from 'features/table'
8 | import { useDialogApi } from 'shared/lib'
9 |
10 | export const useGetStartedButton = () => {
11 | const [tables, setSelectedTableIndex] = useUnit([
12 | $tables,
13 | setSelectedTableIndexEvent,
14 | ])
15 |
16 | const { toggleOpen } = useDialogApi(createTableDialogModel)
17 |
18 | const handleOnClick = () => {
19 | if (tables.length > 0) {
20 | setSelectedTableIndex(0)
21 | } else {
22 | toggleOpen()
23 | }
24 | }
25 |
26 | return {
27 | onClick: handleOnClick,
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/pages/home/ui/welcome-card/index.tsx:
--------------------------------------------------------------------------------
1 | //@ts-ignore
2 | import { UilGithub } from '@iconscout/react-unicons'
3 | import { Button, Card, Stack, Typography, styled } from '@mui/joy'
4 |
5 | import Image from 'next/image'
6 |
7 | import { GetStartedButton } from './get-started-button'
8 | import { taskImage } from 'shared/assets/images'
9 |
10 | export const WelcomeCard = () => {
11 | return (
12 |
13 | <_Image src={taskImage} alt="Task illustration" />
14 | Welcome to Your Personal Task Manager
15 |
16 | Streamline your tasks with our task manager. Stay organized, prioritize
17 | your to-do list, and track progress all in one place. Boost productivity
18 | and achieve your goals effortlessly.
19 |
20 |
21 |
22 | }
27 | variant="outlined"
28 | >
29 | Github
30 |
31 |
32 |
33 | Created by Un1T3G
34 |
35 |
36 | )
37 | }
38 |
39 | const Root = styled(Card)({
40 | position: 'relative',
41 | display: 'flex',
42 | flexDirection: 'column',
43 | alignItems: 'center',
44 | justifyContent: 'center',
45 | flex: 1,
46 | })
47 |
48 | const _Image = styled(Image)({
49 | width: '300px',
50 | height: 'auto',
51 | })
52 |
53 | const AuthorText = styled(Typography)({
54 | position: 'absolute',
55 | bottom: 16,
56 | right: 16,
57 | alignSelf: 'flex-end',
58 | })
59 |
--------------------------------------------------------------------------------
/src/shared/assets/data.json:
--------------------------------------------------------------------------------
1 | [{"id":"cd45c84c-d21f-4ebb-b51d-5412bb1c4663","title":"Platform Launch","columns":[{"id":"5f918302-cf88-4494-ba9a-b81843036c84","title":"Todo","tableId":"cd45c84c-d21f-4ebb-b51d-5412bb1c4663","tasks":[{"id":"2c7cfe36-b0df-412d-b238-9639435f18e2","title":"Build UI for onboarding flow","description":"","columnId":"5f918302-cf88-4494-ba9a-b81843036c84","todos":[{"id":"1913d423-23a6-45b5-b224-24c4aaa597da","title":"Sign up page","isDone":true},{"id":"dab651e8-a154-4e79-ab19-6585ae8105a5","title":"Sign in page","isDone":false},{"id":"642516b4-c0f7-4f1a-8846-262a05e5c4d9","title":"Welcome page","isDone":false}]},{"id":"a63ac4c7-d408-4340-8091-4ad9fc14271d","title":"Build UI for search","description":"","columnId":"5f918302-cf88-4494-ba9a-b81843036c84","todos":[{"id":"6824dcf6-69da-4028-b9e0-78e47eff25ac","title":"Search page","isDone":false}]},{"id":"8b926db3-2e2f-41ef-83c4-f873bba62e73","title":"Build settings UI","description":"","columnId":"5f918302-cf88-4494-ba9a-b81843036c84","todos":[{"id":"81116510-dfce-4290-86de-4b09604e9244","title":"Account page","isDone":false},{"id":"90efd5cb-0c2e-4883-a24e-67df454411ea","title":"Billing page","isDone":false}]},{"id":"2ca6d461-71c0-4e92-bcce-bd521ae02315","title":"QA and test all major user journeys","description":"Once we feel version one is ready, we need to rigorously test it both internally and externally to identify any major gaps.","columnId":"5f918302-cf88-4494-ba9a-b81843036c84","todos":[{"id":"415c45a1-402d-4b62-9045-426a1fec4f49","title":"Internal testing","isDone":false},{"id":"43be24a1-d65f-4277-acf6-c46c01f70cde","title":"External testing","isDone":false}]}]},{"id":"5fdba768-5047-42d1-99e4-8be84f90786b","title":"Doing","tableId":"cd45c84c-d21f-4ebb-b51d-5412bb1c4663","tasks":[{"id":"26f483bb-8cf7-4474-984c-930d4124649e","title":"Design settings and search pages","description":"","columnId":"5fdba768-5047-42d1-99e4-8be84f90786b","todos":[{"id":"c2e6b5e1-071e-4f0a-b451-f33de6e765c1","title":"Settings - Account page","isDone":true},{"id":"15c2f568-934d-4c63-b904-c1a36c85b288","title":"Settings - Billing page","isDone":true},{"id":"340e9e3f-26c5-4ee5-8f19-9f22cdc432e5","title":"Search page","isDone":false}]},{"id":"5bd8be4d-ae58-4fd6-b13d-554e9b88d556","title":"Add account management endpoints","description":"","columnId":"5fdba768-5047-42d1-99e4-8be84f90786b","todos":[{"id":"fffe41f5-8c36-42be-8b63-df61e5fd14d1","title":"Upgrade plan","isDone":true},{"id":"9b556175-f9c2-42eb-ba27-d3065e1172cf","title":"Cancel plan","isDone":true},{"id":"72359a75-45d0-4db0-9ef4-4178233b9ef9","title":"Update payment method","isDone":false}]},{"id":"6c0b9fa9-84d8-4364-8b51-7e1ebb612467","title":"Design onboarding flow","description":"","columnId":"5fdba768-5047-42d1-99e4-8be84f90786b","todos":[{"id":"c50afc94-22d7-492d-b644-887681346bb6","title":"Sign up page","isDone":true},{"id":"75960e88-09cb-461e-ba6a-20682a995a45","title":"Sign in page","isDone":false},{"id":"0de57224-66b8-4347-8acf-5c07520dad86","title":"Welcome page","isDone":false}]},{"id":"5e4f585a-71fe-4a4d-9e5a-abc6981c880e","title":"Add search enpoints","description":"","columnId":"5fdba768-5047-42d1-99e4-8be84f90786b","todos":[{"id":"0d5c232f-c7ce-437c-832f-3bf7c7407996","title":"Add search endpoint","isDone":true},{"id":"b53a5b15-5162-4081-8cb7-f0e04e039777","title":"Define search filters","isDone":false}]},{"id":"e6d89612-5efc-40c8-bdfa-98d13842ed41","title":"Add authentication endpoints","description":"","columnId":"5fdba768-5047-42d1-99e4-8be84f90786b","todos":[{"id":"b2a45349-efc4-454e-8952-ef0795e3a8b5","title":"Define user model","isDone":true},{"id":"8339e1d6-e13c-4667-a115-aa796af44335","title":"Add auth endpoints","isDone":false}]},{"id":"3c915a8f-3a0f-409f-8a97-69d73cf6679f","title":"Research pricing points of various competitors and trial different business models","description":"We know what we're planning to build for version one. Now we need to finalise the first pricing model we'll use. Keep iterating the subtasks until we have a coherent proposition.","columnId":"5fdba768-5047-42d1-99e4-8be84f90786b","todos":[{"id":"e54d856e-6794-47d9-8cb8-3bc4abb2b5de","title":"Research competitor pricing and business models","isDone":true},{"id":"5f8e1287-3c89-4015-9c2d-b79e0dcd73a5","title":"Outline a business model that works for our solution","isDone":false},{"id":"f4230d1e-266a-4264-8264-baf106b87f36","title":"Talk to potential customers about our proposed solution and ask for fair price expectancy","isDone":false}]}]},{"id":"b6e3cc13-8412-40a4-bcee-90dd0d1c6f5f","title":"Done","tableId":"cd45c84c-d21f-4ebb-b51d-5412bb1c4663","tasks":[{"id":"710e80a0-6d5f-4298-af94-30822133b2c8","title":"Conduct 5 wireframe tests","description":"Ensure the layout continues to make sense and we have strong buy-in from potential users.","columnId":"b6e3cc13-8412-40a4-bcee-90dd0d1c6f5f","todos":[{"id":"153715bb-33c5-4376-9273-fcc8dbb34a25","title":"Complete 5 wireframe prototype tests","isDone":true}]},{"id":"25b1d540-56e0-4c0c-aabf-a4e978f1b003","title":"Create wireframe prototype","description":"Create a greyscale clickable wireframe prototype to test our asssumptions so far.","columnId":"b6e3cc13-8412-40a4-bcee-90dd0d1c6f5f","todos":[{"id":"3a03dda3-fa1c-4544-8568-effcec92206b","title":"Create clickable wireframe prototype in Balsamiq","isDone":true}]},{"id":"f6d6f379-84fa-41fd-8957-ba1b5198759f","title":"Review results of usability tests and iterate","description":"Keep iterating through the subtasks until we're clear on the core concepts for the app.","columnId":"b6e3cc13-8412-40a4-bcee-90dd0d1c6f5f","todos":[{"id":"1ea71bd9-1ef7-4ba6-9a34-66089a14e894","title":"Meet to review notes from previous tests and plan changes","isDone":true},{"id":"fea71cd1-b1e1-4a36-a7cb-0f674b7072f8","title":"Make changes to paper prototypes","isDone":true},{"id":"ab9c0e3a-56a9-476c-9b30-857821629cb9","title":"Conduct 5 usability tests","isDone":true}]},{"id":"f053de0f-cd7a-4075-9427-d446c0e81dd2","title":"Create paper prototypes and conduct 10 usability tests with potential customers","description":"","columnId":"b6e3cc13-8412-40a4-bcee-90dd0d1c6f5f","todos":[{"id":"7c4173e2-234b-4fd8-a7c4-190a576bc241","title":"Create paper prototypes for version one","isDone":true},{"id":"db7a18f1-cb8c-40d3-bd1e-a8770a12a432","title":"Complete 10 usability tests","isDone":true}]},{"id":"5f3f733f-bf3c-4ca1-b469-33d0f13e2671","title":"Market discovery","description":"We need to define and refine our core product. Interviews will help us learn common pain points and help us define the strongest MVP.","columnId":"b6e3cc13-8412-40a4-bcee-90dd0d1c6f5f","todos":[{"id":"8ac7d96d-95d7-461f-8681-7a26505bf741","title":"Interview 10 prospective customers","isDone":true}]},{"id":"2ec8637d-c742-463c-bb45-0e993de390db","title":"Competitor analysis","description":"","columnId":"b6e3cc13-8412-40a4-bcee-90dd0d1c6f5f","todos":[{"id":"b2281b16-2369-4509-8411-a4d6a2281174","title":"Find direct and indirect competitors","isDone":true},{"id":"3deec02d-4129-49d8-876f-4b361960724a","title":"SWOT analysis for each competitor","isDone":true}]},{"id":"1cc9feb6-8901-4887-bb95-bd59a7563369","title":"Research the market","description":"We need to get a solid overview of the market to ensure we have up-to-date estimates of market size and demand.","columnId":"b6e3cc13-8412-40a4-bcee-90dd0d1c6f5f","todos":[{"id":"7fca14d3-a858-47b6-a368-507e358575ec","title":"Write up research analysis","isDone":true},{"id":"fa67a5e4-308a-4cd2-977d-8592818edb4e","title":"Calculate TAM","isDone":true}]}]}]},{"id":"d2121e9a-0697-48a6-9534-35474f82930b","title":"Marketing Plan","columns":[{"id":"e55217d4-e98d-486b-99e2-33f760aa81e2","title":"Todo","tableId":"d2121e9a-0697-48a6-9534-35474f82930b","tasks":[{"id":"be190f0a-5a3e-485b-a367-5247f80e2838","title":"Plan Product Hunt launch","description":"","columnId":"e55217d4-e98d-486b-99e2-33f760aa81e2","todos":[{"id":"228444a4-895c-44f3-a332-51e5e01527fb","title":"Find hunter","isDone":false},{"id":"46b6641d-864f-4075-b1ff-eb188b48ef92","title":"Gather assets","isDone":false},{"id":"bc677342-9fa6-4d33-bdb3-c15f605e0b58","title":"Draft product page","isDone":false},{"id":"76f8465d-d7ed-4348-9e66-c7e83f46288e","title":"Notify customers","isDone":false},{"id":"1ae2c67d-c3d9-46ed-9eca-039bb74c1b4c","title":"Notify network","isDone":false},{"id":"198977aa-d471-4762-8c9d-1b4e05bd6365","title":"Launch!","isDone":false}]},{"id":"1f786e08-5855-4789-bce4-c422a7d669c5","title":"Share on Show HN","description":"","columnId":"e55217d4-e98d-486b-99e2-33f760aa81e2","todos":[{"id":"32191a38-74a9-493a-8b5c-e105379bde75","title":"Draft out HN post","isDone":false},{"id":"6181dbe3-fdf4-4656-b69c-1a84e22773c1","title":"Get feedback and refine","isDone":false},{"id":"b9c311c2-d3e7-4dd8-a1d7-a98a72d50f7e","title":"Publish post","isDone":false}]},{"id":"d4b2a8bf-7e3c-4d08-a063-7cc2274b406c","title":"Write launch article to publish on multiple channels","description":"","columnId":"e55217d4-e98d-486b-99e2-33f760aa81e2","todos":[{"id":"588da30c-237b-4948-8ac9-2f744f1e7a49","title":"Write article","isDone":false},{"id":"df3a6ad5-1352-43c0-8685-441726947086","title":"Publish on LinkedIn","isDone":false},{"id":"261ffaa0-1a43-43e0-b27d-4223d0662dc0","title":"Publish on Inndie Hackers","isDone":false},{"id":"e9a4defe-3fa7-4090-bcf8-099717434354","title":"Publish on Medium","isDone":false}]}]},{"id":"c6032772-2d6a-4fa7-867a-cd8b97b4ddaa","title":"Doing","tableId":"d2121e9a-0697-48a6-9534-35474f82930b","tasks":[]},{"id":"de273ada-cd7a-4f1a-9698-1487cd51be87","title":"Done","tableId":"d2121e9a-0697-48a6-9534-35474f82930b","tasks":[]}]},{"id":"eb6a91c8-7b51-41f2-9b0f-ac82f535cb2c","title":"Roadmap","columns":[{"id":"ececb48f-d5a3-438c-81bb-281b4a98702f","title":"Now","tableId":"eb6a91c8-7b51-41f2-9b0f-ac82f535cb2c","tasks":[{"id":"1ab68777-2aac-4d3a-a6e4-efd3cd3c5911","title":"Launch version one","description":"","columnId":"ececb48f-d5a3-438c-81bb-281b4a98702f","todos":[{"id":"038cd79a-8940-437b-8508-e737523552b5","title":"Launch privately to our waitlist","isDone":false},{"id":"ab58eb0f-11fb-4159-ae23-a4c853d1eebe","title":"Launch publicly on PH, HN, etc.","isDone":false}]},{"id":"4fddcd1f-96a6-4fd1-bdd7-fc35571aa0f6","title":"Review early feedback and plan next steps for roadmap","description":"Beyond the initial launch, we're keeping the initial roadmap completely empty. This meeting will help us plan out our next steps based on actual customer feedback.","columnId":"ececb48f-d5a3-438c-81bb-281b4a98702f","todos":[{"id":"34f36b4b-2fa7-43a1-a7a9-f93a1a289944","title":"Interview 10 customers","isDone":false},{"id":"00323e1e-99c2-4609-8e6a-96e2a7d3331b","title":"Review common customer pain points and suggestions","isDone":false},{"id":"c9748e82-390f-476e-80b1-5d23e0790895","title":"Outline next steps for our roadmap","isDone":false}]}]},{"id":"4e603dec-4c3b-4264-b8e0-22978b3fe3e1","title":"Next","tableId":"eb6a91c8-7b51-41f2-9b0f-ac82f535cb2c","tasks":[]},{"id":"f1d92373-0a16-4953-bee0-653e1399f2fb","title":"Later","tableId":"eb6a91c8-7b51-41f2-9b0f-ac82f535cb2c","tasks":[]}]}]
--------------------------------------------------------------------------------
/src/shared/assets/images/index.ts:
--------------------------------------------------------------------------------
1 | import brandLogo from './logo.svg'
2 | import taskImage from './task.png'
3 |
4 | export { brandLogo, taskImage }
5 |
--------------------------------------------------------------------------------
/src/shared/assets/images/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/shared/assets/images/task.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Un1T3G/nextjs-task-management-app/7315f8bdcfe9a7047bfcafed83e81f326f7eab76/src/shared/assets/images/task.png
--------------------------------------------------------------------------------
/src/shared/assets/images/task.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 | } onClick={toggleOpen}>
20 | Create new task
21 |
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 |
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 |
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 |
--------------------------------------------------------------------------------