diff --git a/.prettierrc b/.prettierrc index d0cdbaeb..c03bd2b5 100644 --- a/.prettierrc +++ b/.prettierrc @@ -18,9 +18,9 @@ "^(react)(/.*)?$", "", "", - "^components(/.*)?$", + "^features(/.*)?$", "", - "^hooks(/.*)?$", + "^(pages|blocks|components|hooks)(/.*)?$", "", "^redux(/.*)?$", "", diff --git a/src/api/index.ts b/src/api/index.ts index b3a3204f..55a359be 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,11 +1,11 @@ import axios from 'axios' import createAuthRefreshInterceptor from 'axios-auth-refresh' +import { authService } from 'features/auth/auth.service' + import { store } from 'redux/store' import { logout, saveTokens } from 'redux/user.slice' -import { authService } from 'services' - export const axiosInstance = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL }) diff --git a/src/components/dashboard/header/Header.tsx b/src/blocks/header/Header.tsx similarity index 82% rename from src/components/dashboard/header/Header.tsx rename to src/blocks/header/Header.tsx index 6ee42621..afe41be1 100644 --- a/src/components/dashboard/header/Header.tsx +++ b/src/blocks/header/Header.tsx @@ -1,5 +1,6 @@ +import { ThemeSelect } from 'features/user/components' + import { HeaderControls } from './HeaderControls' -import { HeaderThemeSelect } from './HeaderThemeSelect' import { HeaderUserInfo } from './HeaderUserInfo' export const Header = () => ( @@ -8,7 +9,7 @@ export const Header = () => ( duration-300 dark:bg-black'>
- +
diff --git a/src/components/dashboard/header/HeaderControls.tsx b/src/blocks/header/HeaderControls.tsx similarity index 94% rename from src/components/dashboard/header/HeaderControls.tsx rename to src/blocks/header/HeaderControls.tsx index be233637..f2293bc7 100644 --- a/src/components/dashboard/header/HeaderControls.tsx +++ b/src/blocks/header/HeaderControls.tsx @@ -1,12 +1,11 @@ import { BiSidebar } from 'react-icons/bi' import { useModal } from 'react-modal-state' +import { BurgerMenu } from 'blocks/sidebar/BurgerMenu' import { useAppDispatch, useAppSelector } from 'hooks/redux' import { selectIsSidebarOpen, setIsSidebarOpen } from 'redux/sidebar.slice' -import { BurgerMenu } from '../modals/BurgerMenu' - export const HeaderControls = () => { const { open } = useModal(BurgerMenu) diff --git a/src/components/dashboard/header/HeaderUserInfo.tsx b/src/blocks/header/HeaderUserInfo.tsx similarity index 93% rename from src/components/dashboard/header/HeaderUserInfo.tsx rename to src/blocks/header/HeaderUserInfo.tsx index f922b277..004afdf8 100644 --- a/src/components/dashboard/header/HeaderUserInfo.tsx +++ b/src/blocks/header/HeaderUserInfo.tsx @@ -1,12 +1,12 @@ import * as Avatar from '@radix-ui/react-avatar' import { useModal } from 'react-modal-state' +import { EditProfileModal } from 'features/user/components/modals' + import { useAppSelector } from 'hooks/redux' import { selectUser } from 'redux/user.slice' -import { EditProfileModal } from '../modals' - export const HeaderUserInfo = () => { const { name, avatar } = useAppSelector(selectUser) diff --git a/src/components/dashboard/index.ts b/src/blocks/index.ts similarity index 68% rename from src/components/dashboard/index.ts rename to src/blocks/index.ts index d09c2b89..8d22211f 100644 --- a/src/components/dashboard/index.ts +++ b/src/blocks/index.ts @@ -1,3 +1,2 @@ -export * from './board/Board' export * from './header/Header' export * from './sidebar/Sidebar' diff --git a/src/components/dashboard/modals/BurgerMenu.tsx b/src/blocks/sidebar/BurgerMenu.tsx similarity index 64% rename from src/components/dashboard/modals/BurgerMenu.tsx rename to src/blocks/sidebar/BurgerMenu.tsx index 65d3ad0e..f3492817 100644 --- a/src/components/dashboard/modals/BurgerMenu.tsx +++ b/src/blocks/sidebar/BurgerMenu.tsx @@ -1,11 +1,11 @@ import { useModalInstance } from 'react-modal-state' import { Modal } from 'react-responsive-modal' -import { SidebarBoardsList } from 'components/dashboard/sidebar/SidebarBoardsList' -import { SidebarLogo } from 'components/dashboard/sidebar/SidebarLogo' -import { SidebarLogoutBtn } from 'components/dashboard/sidebar/SidebarLogoutBtn' -import { SidebarMyBoardsInfo } from 'components/dashboard/sidebar/SidebarMyBoardsInfo' -import { SidebarUserSupport } from 'components/dashboard/sidebar/SidebarUserSupport' +import { SidebarBoardsList } from './SidebarBoardsList' +import { SidebarLogo } from './SidebarLogo' +import { SidebarLogoutBtn } from './SidebarLogoutBtn' +import { SidebarMyBoardsInfo } from './SidebarMyBoardsInfo' +import { SidebarUserSupport } from './SidebarUserSupport' export const BurgerMenu = () => { const { isOpen, close } = useModalInstance() diff --git a/src/components/dashboard/sidebar/Sidebar.tsx b/src/blocks/sidebar/Sidebar.tsx similarity index 99% rename from src/components/dashboard/sidebar/Sidebar.tsx rename to src/blocks/sidebar/Sidebar.tsx index e1218c2c..aeff83b5 100644 --- a/src/components/dashboard/sidebar/Sidebar.tsx +++ b/src/blocks/sidebar/Sidebar.tsx @@ -1,7 +1,6 @@ import * as ScrollArea from '@radix-ui/react-scroll-area' import { Scrollbar } from 'components/ui' - import { useAppSelector } from 'hooks/redux' import { selectIsSidebarOpen } from 'redux/sidebar.slice' diff --git a/src/components/dashboard/sidebar/SidebarBoardsList.tsx b/src/blocks/sidebar/SidebarBoardsList.tsx similarity index 85% rename from src/components/dashboard/sidebar/SidebarBoardsList.tsx rename to src/blocks/sidebar/SidebarBoardsList.tsx index 67ac35fd..24ddffc2 100644 --- a/src/components/dashboard/sidebar/SidebarBoardsList.tsx +++ b/src/blocks/sidebar/SidebarBoardsList.tsx @@ -1,18 +1,16 @@ import { Indicator, Item, Root } from '@radix-ui/react-radio-group' -import { useQuery } from '@tanstack/react-query' import { useModal } from 'react-modal-state' import { useNavigate } from 'react-router-dom' -import { Loader } from 'components/ui' +import { useGetAllBoards, useGetBoardId } from 'features/kanban/board/hooks' -import { useGetBoardId } from 'hooks/board' +import { Loader } from 'components/ui' -import { CacheKeys, Pages } from 'config' -import { boardService } from 'services' +import { Pages } from 'config' import { cn } from 'lib' -import { BurgerMenu } from '../modals' +import { BurgerMenu } from './BurgerMenu' import { SidebarListActiveItem } from './SidebarListActiveItem' export const SidebarBoardsList = () => { @@ -22,10 +20,7 @@ export const SidebarBoardsList = () => { const { close: closeBurgerMenu } = useModal(BurgerMenu) - const { data, isPending } = useQuery({ - queryKey: [CacheKeys.Boards], - queryFn: boardService.getAllBoards - }) + const { data, isPending } = useGetAllBoards() return isPending ? (
diff --git a/src/components/dashboard/sidebar/SidebarListActiveItem.tsx b/src/blocks/sidebar/SidebarListActiveItem.tsx similarity index 88% rename from src/components/dashboard/sidebar/SidebarListActiveItem.tsx rename to src/blocks/sidebar/SidebarListActiveItem.tsx index b0fa5ff4..5e264a37 100644 --- a/src/components/dashboard/sidebar/SidebarListActiveItem.tsx +++ b/src/blocks/sidebar/SidebarListActiveItem.tsx @@ -1,10 +1,11 @@ -import type { Board } from 'types' +import type { Board } from 'features/kanban/board/board.types' import { useModal } from 'react-modal-state' -import { useDeleteBoard } from 'hooks/board' +import { EditBoardModal } from 'features/kanban/board/components/modals' +import { useDeleteBoard } from 'features/kanban/board/hooks' -import { BurgerMenu, EditBoardModal } from '../modals' +import { BurgerMenu } from './BurgerMenu' export const SidebarListActiveItem = ({ board: { icon, title, background } diff --git a/src/components/dashboard/sidebar/SidebarLogo.tsx b/src/blocks/sidebar/SidebarLogo.tsx similarity index 97% rename from src/components/dashboard/sidebar/SidebarLogo.tsx rename to src/blocks/sidebar/SidebarLogo.tsx index 8e7b9ccd..11e0c9e5 100644 --- a/src/components/dashboard/sidebar/SidebarLogo.tsx +++ b/src/blocks/sidebar/SidebarLogo.tsx @@ -3,7 +3,7 @@ import { Link } from 'react-router-dom' import { Pages } from 'config' -import { BurgerMenu } from '../modals' +import { BurgerMenu } from './BurgerMenu' export const SidebarLogo = () => { const { close: closeBurgerMenu } = useModal(BurgerMenu) diff --git a/src/components/dashboard/sidebar/SidebarLogoutBtn.tsx b/src/blocks/sidebar/SidebarLogoutBtn.tsx similarity index 92% rename from src/components/dashboard/sidebar/SidebarLogoutBtn.tsx rename to src/blocks/sidebar/SidebarLogoutBtn.tsx index cc284a6f..1e9c17b0 100644 --- a/src/components/dashboard/sidebar/SidebarLogoutBtn.tsx +++ b/src/blocks/sidebar/SidebarLogoutBtn.tsx @@ -1,4 +1,4 @@ -import { useLogoutUser } from 'hooks/auth' +import { useLogoutUser } from 'features/auth/hooks' export const SidebarLogoutBtn = () => { const { mutate, isPending } = useLogoutUser() diff --git a/src/components/dashboard/sidebar/SidebarMyBoardsInfo.tsx b/src/blocks/sidebar/SidebarMyBoardsInfo.tsx similarity index 91% rename from src/components/dashboard/sidebar/SidebarMyBoardsInfo.tsx rename to src/blocks/sidebar/SidebarMyBoardsInfo.tsx index 342b035d..524f005b 100644 --- a/src/components/dashboard/sidebar/SidebarMyBoardsInfo.tsx +++ b/src/blocks/sidebar/SidebarMyBoardsInfo.tsx @@ -1,8 +1,10 @@ import { useModal } from 'react-modal-state' +import { NewBoardModal } from 'features/kanban/board/components/modals' + import { cn } from 'lib' -import { BurgerMenu, NewBoardModal } from '../modals' +import { BurgerMenu } from './BurgerMenu' export const SidebarMyBoardsInfo = () => { const { open } = useModal(NewBoardModal) diff --git a/src/components/dashboard/sidebar/SidebarUserSupport.tsx b/src/blocks/sidebar/SidebarUserSupport.tsx similarity index 92% rename from src/components/dashboard/sidebar/SidebarUserSupport.tsx rename to src/blocks/sidebar/SidebarUserSupport.tsx index 01e4f129..7bf5be63 100644 --- a/src/components/dashboard/sidebar/SidebarUserSupport.tsx +++ b/src/blocks/sidebar/SidebarUserSupport.tsx @@ -1,6 +1,8 @@ import { useModal } from 'react-modal-state' -import { BurgerMenu, NeedHelpModal } from '../modals' +import { NeedHelpModal } from 'features/user/components/modals' + +import { BurgerMenu } from './BurgerMenu' export const SidebarUserSupport = () => { const { open } = useModal(NeedHelpModal) diff --git a/src/components/App.tsx b/src/components/App.tsx index 077f7ecc..44a6a5da 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,37 +1,18 @@ -import { useEffect } from 'react' -import { useQuery } from '@tanstack/react-query' -import { AuthPage, DashboardPage, HomePage } from 'pages' import { Route, Routes } from 'react-router-dom' -import { PrivateRoute, PublicOnlyRoute } from 'components/routes' - -import { useAppDispatch, useAppSelector } from 'hooks/redux' +import { Board } from 'features/kanban/board/components/Board' +import { useGetCurrentUser } from 'features/user/hooks' -import { selectIsLoggedIn, updateUser } from 'redux/user.slice' +import { PrivateRoute, PublicOnlyRoute } from 'components/routes' +import { AuthPage, DashboardPage, HomePage } from 'pages' -import { CacheKeys, Pages } from 'config' -import { userService } from 'services' +import { Pages } from 'config' -import { Board } from './dashboard' -import { EmptyBoard } from './dashboard/board/EmptyBoard' +import { EmptyBoard } from '../features/kanban/board/components/EmptyBoard' import { Layout } from './Layout' export const App = () => { - const isLoggedIn = useAppSelector(selectIsLoggedIn) - - const dispatch = useAppDispatch() - - const { data, isSuccess } = useQuery({ - queryKey: [CacheKeys.User], - queryFn: userService.getCurrentUser, - enabled: isLoggedIn - }) - - useEffect(() => { - if (isSuccess) { - dispatch(updateUser(data)) - } - }, [data, dispatch, isSuccess]) + useGetCurrentUser() return ( diff --git a/src/components/dashboard/modals/index.ts b/src/components/dashboard/modals/index.ts deleted file mode 100644 index d218c49c..00000000 --- a/src/components/dashboard/modals/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './board/EditBoard' -export * from './board/NewBoard' -export * from './BurgerMenu' -export * from './card/EditCard' -export * from './card/NewCard' -export * from './edit-profile/EditProfileModal' -export * from './NeedHelp' diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index 8f3e0bd4..60990279 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -1,7 +1,5 @@ export * from './Button' -export * from './Scrollbar' -export * from './DatePicker' export * from './Field' export * from './Loader' export * from './Modal' -export * from './RadioInput' +export * from './Scrollbar' diff --git a/src/config/api-endpoints.config.ts b/src/config/api-endpoints.config.ts deleted file mode 100644 index 72788c35..00000000 --- a/src/config/api-endpoints.config.ts +++ /dev/null @@ -1,27 +0,0 @@ -export const ApiEndpoints = { - Signup: '/auth/signup', - Signin: '/auth/signin', - Tokens: '/auth/tokens', - Logout: '/auth/logout', - Google: '/auth/google', - - User: '/user', - UserMe: '/user/me', - UserTheme: '/user/theme', - UserHelp: '/user/help', - - Board: '/board', - BoardById: (boardId: string) => `${ApiEndpoints.Board}/${boardId}`, - - Column: '/column', - ColumnById: (columnId: string) => `${ApiEndpoints.Column}/${columnId}`, - ColumnBoardById: (boardId: string) => `${ApiEndpoints.Column}/${boardId}`, - ColumnOrder: (boardId: string) => - `${ApiEndpoints.ColumnBoardById(boardId)}/order`, - - Card: '/card', - CardById: (cardId: string) => `${ApiEndpoints.Card}/${cardId}`, - CardColumnById: (columnId: string) => `${ApiEndpoints.Card}/${columnId}`, - CardOrder: (columnId: string) => - `${ApiEndpoints.CardColumnById(columnId)}/order` -} diff --git a/src/config/cache-keys.config.ts b/src/config/cache-keys.config.ts deleted file mode 100644 index 15591969..00000000 --- a/src/config/cache-keys.config.ts +++ /dev/null @@ -1,26 +0,0 @@ -export enum CacheKeys { - Signup = 'signup', - Signin = 'signin', - Logout = 'logout', - - User = 'user', - UserHelp = 'userHelp', - EditUserProfile = 'editUserProfile', - ChangeUserTheme = 'changeUserTheme', - - Boards = 'boards', - Board = 'board', - AddBoard = 'addBoard', - EditBoard = 'editBoard', - DeleteBoard = 'deleteBoard', - - AddCard = 'addCard', - EditCard = 'editCard', - DeleteCard = 'deleteCard', - UpdateCardsOrder = 'updateCardsOrder', - - AddColumn = 'addColumn', - EditColumn = 'editColumn', - DeleteColumn = 'deleteColumn', - UpdateColumnsOrder = 'updateColumnsOrder' -} diff --git a/src/config/index.ts b/src/config/index.ts index 2ed2664b..e65a97fe 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,3 +1 @@ -export * from './api-endpoints.config' -export * from './cache-keys.config' export * from './pages-url.config' diff --git a/src/constants/deadlines.ts b/src/constants/deadlines.ts deleted file mode 100644 index c30ece4e..00000000 --- a/src/constants/deadlines.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const deadlines = ['Upcoming', 'Overdue', 'Far Future'] as const - -export type Deadline = (typeof deadlines)[number] diff --git a/src/constants/priorities.ts b/src/constants/priorities.ts deleted file mode 100644 index 1a28da06..00000000 --- a/src/constants/priorities.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const priorities = ['High', 'Medium', 'Low', 'Without'] as const - -export type Priority = (typeof priorities)[number] diff --git a/src/constants/themes.ts b/src/constants/themes.ts deleted file mode 100644 index ea659564..00000000 --- a/src/constants/themes.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const themes = ['light', 'dark', 'violet'] as const - -export type Theme = (typeof themes)[number] diff --git a/src/lib/schemas/user.schema.ts b/src/features/auth/auth.schema.ts similarity index 61% rename from src/lib/schemas/user.schema.ts rename to src/features/auth/auth.schema.ts index cd4488c5..4e303f1b 100644 --- a/src/lib/schemas/user.schema.ts +++ b/src/features/auth/auth.schema.ts @@ -18,22 +18,5 @@ export const SignupSchema = v.object({ ...SigninSchema.entries }) -export const EditUserSchema = v.partial( - v.object({ - ...SignupSchema.entries, - avatar: v.instance(File) - }) -) - -export const HelpSchema = v.object({ - email: SigninSchema.entries.email, - comment: v.string([ - v.toTrimmed(), - v.minLength(5, 'Please enter at least 5 characters.') - ]) -}) - export type SigninSchema = v.Output export type SignupSchema = v.Output -export type EditUserSchema = v.Output -export type HelpSchema = v.Output diff --git a/src/services/auth.service.ts b/src/features/auth/auth.service.ts similarity index 67% rename from src/services/auth.service.ts rename to src/features/auth/auth.service.ts index 29b4fb66..bd1e2645 100644 --- a/src/services/auth.service.ts +++ b/src/features/auth/auth.service.ts @@ -1,14 +1,15 @@ import type { AxiosAuthRefreshRequestConfig } from 'axios-auth-refresh' -import type { SigninSchema, SignupSchema } from 'lib/schemas' -import type { AuthResponse, Tokens } from 'types' +import type { SigninSchema, SignupSchema } from './auth.schema' +import type { AuthResponse, Tokens } from './auth.types' import { axiosInstance } from 'api' -import { ApiEndpoints } from 'config' + +import { AuthApiEndpoints } from './config' export const authService = { async signup(data: SignupSchema) { const response = await axiosInstance.post( - ApiEndpoints.Signup, + AuthApiEndpoints.Signup, data ) @@ -21,7 +22,7 @@ export const authService = { } const response = await axiosInstance.post( - ApiEndpoints.Signin, + AuthApiEndpoints.Signin, data, requestConfig ) @@ -31,7 +32,7 @@ export const authService = { async signinWithGoogle(code: string) { const response = await axiosInstance.post( - ApiEndpoints.Google, + AuthApiEndpoints.Google, { code } ) @@ -39,10 +40,10 @@ export const authService = { }, getTokens(data: { refreshToken: string }) { - return axiosInstance.post(ApiEndpoints.Tokens, data) + return axiosInstance.post(AuthApiEndpoints.Tokens, data) }, async logout() { - await axiosInstance.post(ApiEndpoints.Logout) + await axiosInstance.post(AuthApiEndpoints.Logout) } } diff --git a/src/features/auth/auth.types.ts b/src/features/auth/auth.types.ts new file mode 100644 index 00000000..558c291a --- /dev/null +++ b/src/features/auth/auth.types.ts @@ -0,0 +1,9 @@ +import type { User } from 'features/user/user.types' + +export type AuthResponse = { + refreshToken: string + accessToken: string + user: User +} + +export type Tokens = Omit diff --git a/src/components/auth/AuthFormNavigation.tsx b/src/features/auth/components/AuthFormNavigation.tsx similarity index 100% rename from src/components/auth/AuthFormNavigation.tsx rename to src/features/auth/components/AuthFormNavigation.tsx diff --git a/src/components/auth/AuthLayout.tsx b/src/features/auth/components/AuthLayout.tsx similarity index 100% rename from src/components/auth/AuthLayout.tsx rename to src/features/auth/components/AuthLayout.tsx diff --git a/src/components/auth/GoogleSignin.tsx b/src/features/auth/components/GoogleSignin.tsx similarity index 95% rename from src/components/auth/GoogleSignin.tsx rename to src/features/auth/components/GoogleSignin.tsx index 825af57d..c8a42fbd 100644 --- a/src/components/auth/GoogleSignin.tsx +++ b/src/features/auth/components/GoogleSignin.tsx @@ -6,7 +6,7 @@ import { useAppDispatch } from 'hooks/redux' import { authenticate } from 'redux/user.slice' -import { authService } from 'services' +import { authService } from '../auth.service' export const GoogleSignin = () => { const dispatch = useAppDispatch() diff --git a/src/components/auth/SigninForm.tsx b/src/features/auth/components/SigninForm.tsx similarity index 91% rename from src/components/auth/SigninForm.tsx rename to src/features/auth/components/SigninForm.tsx index 2002d7ea..ff3ef015 100644 --- a/src/components/auth/SigninForm.tsx +++ b/src/features/auth/components/SigninForm.tsx @@ -1,9 +1,9 @@ -import { Button, Field, Loader } from 'components/ui' +import { useSigninUser } from 'features/auth/hooks' +import { Button, Field, Loader } from 'components/ui' import { useAppForm } from 'hooks' -import { useSigninUser } from 'hooks/auth' -import { SigninSchema } from 'lib/schemas' +import { SigninSchema } from '../auth.schema' export const SigninForm = () => { const { handleSubmit, register, formState, reset } = useAppForm(SigninSchema) diff --git a/src/components/auth/SignupForm.tsx b/src/features/auth/components/SignupForm.tsx similarity index 92% rename from src/components/auth/SignupForm.tsx rename to src/features/auth/components/SignupForm.tsx index a288711b..9bbba953 100644 --- a/src/components/auth/SignupForm.tsx +++ b/src/features/auth/components/SignupForm.tsx @@ -1,9 +1,9 @@ -import { Button, Field, Loader } from 'components/ui' +import { useSignupUser } from 'features/auth/hooks' +import { Button, Field, Loader } from 'components/ui' import { useAppForm } from 'hooks' -import { useSignupUser } from 'hooks/auth' -import { SignupSchema } from 'lib/schemas' +import { SignupSchema } from '../auth.schema' export const SignupForm = () => { const { handleSubmit, register, formState, reset } = useAppForm(SignupSchema) diff --git a/src/components/auth/index.ts b/src/features/auth/components/index.ts similarity index 80% rename from src/components/auth/index.ts rename to src/features/auth/components/index.ts index d6a663a4..9176eaab 100644 --- a/src/components/auth/index.ts +++ b/src/features/auth/components/index.ts @@ -1,4 +1,5 @@ export * from './AuthFormNavigation' export * from './AuthLayout' +export * from './GoogleSignin' export * from './SigninForm' export * from './SignupForm' diff --git a/src/features/auth/config/auth-api-endpoints.config.ts b/src/features/auth/config/auth-api-endpoints.config.ts new file mode 100644 index 00000000..7c871f4a --- /dev/null +++ b/src/features/auth/config/auth-api-endpoints.config.ts @@ -0,0 +1,7 @@ +export enum AuthApiEndpoints { + Signup = '/auth/signup', + Signin = '/auth/signin', + Tokens = '/auth/tokens', + Logout = '/auth/logout', + Google = '/auth/google' +} diff --git a/src/features/auth/config/auth-cache-keys.config.ts b/src/features/auth/config/auth-cache-keys.config.ts new file mode 100644 index 00000000..d32ec728 --- /dev/null +++ b/src/features/auth/config/auth-cache-keys.config.ts @@ -0,0 +1,5 @@ +export enum AuthCacheKeys { + Signup = 'signup', + Signin = 'signin', + Logout = 'logout' +} diff --git a/src/features/auth/config/index.ts b/src/features/auth/config/index.ts new file mode 100644 index 00000000..e964ffe5 --- /dev/null +++ b/src/features/auth/config/index.ts @@ -0,0 +1,2 @@ +export * from './auth-api-endpoints.config' +export * from './auth-cache-keys.config' diff --git a/src/hooks/auth/index.ts b/src/features/auth/hooks/index.ts similarity index 100% rename from src/hooks/auth/index.ts rename to src/features/auth/hooks/index.ts diff --git a/src/hooks/auth/useLogoutUser.ts b/src/features/auth/hooks/useLogoutUser.ts similarity index 78% rename from src/hooks/auth/useLogoutUser.ts rename to src/features/auth/hooks/useLogoutUser.ts index db6e17bf..0d9d00c2 100644 --- a/src/hooks/auth/useLogoutUser.ts +++ b/src/features/auth/hooks/useLogoutUser.ts @@ -2,14 +2,13 @@ import { useMutation } from '@tanstack/react-query' import { useModal } from 'react-modal-state' import { toast } from 'sonner' -import { BurgerMenu } from 'components/dashboard/modals' - +import { BurgerMenu } from 'blocks/sidebar/BurgerMenu' import { useAppDispatch } from 'hooks/redux' import { logout } from 'redux/user.slice' -import { CacheKeys } from 'config' -import { authService } from 'services' +import { authService } from '../auth.service' +import { AuthCacheKeys } from '../config' export const useLogoutUser = () => { const dispatch = useAppDispatch() @@ -17,7 +16,7 @@ export const useLogoutUser = () => { const { close: closeBurgerMenu } = useModal(BurgerMenu) return useMutation({ - mutationKey: [CacheKeys.Logout], + mutationKey: [AuthCacheKeys.Logout], mutationFn: authService.logout, onSuccess() { dispatch(logout()) diff --git a/src/hooks/auth/useSigninUser.ts b/src/features/auth/hooks/useSigninUser.ts similarity index 81% rename from src/hooks/auth/useSigninUser.ts rename to src/features/auth/hooks/useSigninUser.ts index 4044138b..43f95d44 100644 --- a/src/hooks/auth/useSigninUser.ts +++ b/src/features/auth/hooks/useSigninUser.ts @@ -1,5 +1,5 @@ -import type { SigninSchema } from 'lib/schemas' import type { UseFormReset } from 'react-hook-form' +import type { SigninSchema } from '../auth.schema' import { useMutation } from '@tanstack/react-query' import { toast } from 'sonner' @@ -8,14 +8,14 @@ import { useAppDispatch } from 'hooks/redux' import { authenticate } from 'redux/user.slice' -import { CacheKeys } from 'config' -import { authService } from 'services' +import { authService } from '../auth.service' +import { AuthCacheKeys } from '../config' export const useSigninUser = (reset: UseFormReset) => { const dispatch = useAppDispatch() return useMutation({ - mutationKey: [CacheKeys.Signin], + mutationKey: [AuthCacheKeys.Signin], mutationFn: authService.signin, onSuccess(data) { reset() diff --git a/src/hooks/auth/useSignupUser.ts b/src/features/auth/hooks/useSignupUser.ts similarity index 81% rename from src/hooks/auth/useSignupUser.ts rename to src/features/auth/hooks/useSignupUser.ts index de00e991..705c3c0d 100644 --- a/src/hooks/auth/useSignupUser.ts +++ b/src/features/auth/hooks/useSignupUser.ts @@ -1,5 +1,5 @@ -import type { SignupSchema } from 'lib/schemas' import type { UseFormReset } from 'react-hook-form' +import type { SignupSchema } from '../auth.schema' import { useMutation } from '@tanstack/react-query' import { toast } from 'sonner' @@ -8,14 +8,14 @@ import { useAppDispatch } from 'hooks/redux' import { authenticate } from 'redux/user.slice' -import { CacheKeys } from 'config' -import { authService } from 'services' +import { authService } from '../auth.service' +import { AuthCacheKeys } from '../config' export const useSignupUser = (reset: UseFormReset) => { const dispatch = useAppDispatch() return useMutation({ - mutationKey: [CacheKeys.Signup], + mutationKey: [AuthCacheKeys.Signup], mutationFn: authService.signup, onSuccess(data) { reset() diff --git a/src/constants/icons.ts b/src/features/kanban/board/board.constants.ts similarity index 63% rename from src/constants/icons.ts rename to src/features/kanban/board/board.constants.ts index 67182c0f..f730c8e4 100644 --- a/src/constants/icons.ts +++ b/src/features/kanban/board/board.constants.ts @@ -1,4 +1,4 @@ -export const icons = [ +export const ICONS = [ 'project', 'star', 'loading', @@ -9,4 +9,4 @@ export const icons = [ 'hexagon' ] as const -export type Icon = (typeof icons)[number] +export type Icon = (typeof ICONS)[number] diff --git a/src/features/kanban/board/board.schema.ts b/src/features/kanban/board/board.schema.ts new file mode 100644 index 00000000..6132067f --- /dev/null +++ b/src/features/kanban/board/board.schema.ts @@ -0,0 +1,12 @@ +import * as v from 'valibot' + +import { TitleSchema } from '../shared/schema' +import { ICONS } from './board.constants' + +export const BoardSchema = v.object({ + ...TitleSchema.entries, + icon: v.picklist(ICONS), + background: v.string() +}) + +export type BoardSchema = v.Output diff --git a/src/services/board.service.ts b/src/features/kanban/board/board.service.ts similarity index 52% rename from src/services/board.service.ts rename to src/features/kanban/board/board.service.ts index 5e72ff99..8298d2d5 100644 --- a/src/services/board.service.ts +++ b/src/features/kanban/board/board.service.ts @@ -1,33 +1,37 @@ -import type { BoardSchema } from 'lib/schemas' -import type { Board } from 'types' +import type { BoardSchema } from './board.schema' +import type { Board } from './board.types' import { axiosInstance } from 'api' -import { ApiEndpoints } from 'config' + +import { BoardApiEndpoints } from './config' export const boardService = { async getAllBoards() { - const response = await axiosInstance.get(ApiEndpoints.Board) + const response = await axiosInstance.get(BoardApiEndpoints.Board) return response.data }, async getBoardById(boardId: string) { const response = await axiosInstance.get( - ApiEndpoints.BoardById(boardId) + BoardApiEndpoints.BoardById(boardId) ) return response.data }, async addNewBoard(data: BoardSchema) { - const response = await axiosInstance.post(ApiEndpoints.Board, data) + const response = await axiosInstance.post( + BoardApiEndpoints.Board, + data + ) return response.data }, async editBoard(boardId: string, data: BoardSchema) { const response = await axiosInstance.put( - ApiEndpoints.BoardById(boardId), + BoardApiEndpoints.BoardById(boardId), data ) @@ -35,6 +39,6 @@ export const boardService = { }, async deleteBoard(boardId: string) { - await axiosInstance.delete(ApiEndpoints.BoardById(boardId)) + await axiosInstance.delete(BoardApiEndpoints.BoardById(boardId)) } } diff --git a/src/features/kanban/board/board.types.ts b/src/features/kanban/board/board.types.ts new file mode 100644 index 00000000..9e0caac4 --- /dev/null +++ b/src/features/kanban/board/board.types.ts @@ -0,0 +1,13 @@ +import type { Column } from '../column/column.types' + +export type Board = { + id: string + title: string + icon: string + background: { + hasWhiteTextColor: boolean + identifier: string + url: string + } + columns: Column[] +} diff --git a/src/components/dashboard/board/Board.tsx b/src/features/kanban/board/components/Board.tsx similarity index 84% rename from src/components/dashboard/board/Board.tsx rename to src/features/kanban/board/components/Board.tsx index 9c04401d..76254657 100644 --- a/src/components/dashboard/board/Board.tsx +++ b/src/features/kanban/board/components/Board.tsx @@ -1,16 +1,15 @@ import * as ScrollArea from '@radix-ui/react-scroll-area' -import { DragAndDropProvider } from 'context/dnd.context' -import { Loader, Scrollbar } from 'components/ui' +import { useGetBoardById } from 'features/kanban/board/hooks' +import { BoardColumnsList } from 'features/kanban/column/components/BoardColumnsList' +import { DragAndDropProvider } from 'features/kanban/dnd/dnd.context' +import { Filters } from 'features/kanban/filters/components/Filters' +import { Loader, Scrollbar } from 'components/ui' import { useDocumentTitle } from 'hooks' -import { useGetBoardById } from 'hooks/board' import { cn } from 'lib' -import { BoardColumnsList } from './columns/BoardColumnsList' -import { Filters } from './filters/Filters' - export const Board = () => { const { data, isPending } = useGetBoardById() diff --git a/src/components/dashboard/board/EmptyBoard.tsx b/src/features/kanban/board/components/EmptyBoard.tsx similarity index 93% rename from src/components/dashboard/board/EmptyBoard.tsx rename to src/features/kanban/board/components/EmptyBoard.tsx index c9343baf..2ad14d92 100644 --- a/src/components/dashboard/board/EmptyBoard.tsx +++ b/src/features/kanban/board/components/EmptyBoard.tsx @@ -1,6 +1,6 @@ import { useModal } from 'react-modal-state' -import { NewBoardModal } from 'components/dashboard/modals' +import { NewBoardModal } from './modals' export const EmptyBoard = () => { const { open } = useModal(NewBoardModal) diff --git a/src/components/dashboard/modals/board/EditBoard.tsx b/src/features/kanban/board/components/modals/EditBoardModal.tsx similarity index 89% rename from src/components/dashboard/modals/board/EditBoard.tsx rename to src/features/kanban/board/components/modals/EditBoardModal.tsx index 97c2caf9..ca6f51cb 100644 --- a/src/components/dashboard/modals/board/EditBoard.tsx +++ b/src/features/kanban/board/components/modals/EditBoardModal.tsx @@ -1,15 +1,14 @@ -import type { Icon } from 'constants/icons' +import type { Icon } from 'features/kanban/board/board.constants' import { useEffect } from 'react' import { useModalInstance } from 'react-modal-state' import { keyof } from 'valibot' -import { Button, Field, Modal } from 'components/ui' +import { BoardSchema } from 'features/kanban/board/board.schema' +import { useEditBoard } from 'features/kanban/board/hooks' +import { Button, Field, Modal } from 'components/ui' import { useAppForm } from 'hooks' -import { useEditBoard } from 'hooks/board' - -import { BoardSchema } from 'lib/schemas' import { RadioInputBgImages } from './RadioInputBgImages' import { RadioInputIcons } from './RadioInputIcons' diff --git a/src/components/dashboard/modals/board/NewBoard.tsx b/src/features/kanban/board/components/modals/NewBoardModal.tsx similarity index 89% rename from src/components/dashboard/modals/board/NewBoard.tsx rename to src/features/kanban/board/components/modals/NewBoardModal.tsx index 09f2361f..32d28e5c 100644 --- a/src/components/dashboard/modals/board/NewBoard.tsx +++ b/src/features/kanban/board/components/modals/NewBoardModal.tsx @@ -1,9 +1,8 @@ -import { Button, Field, Modal } from 'components/ui' +import { BoardSchema } from 'features/kanban/board/board.schema' +import { useAddBoard } from 'features/kanban/board/hooks' +import { Button, Field, Modal } from 'components/ui' import { useAppForm } from 'hooks' -import { useAddBoard } from 'hooks/board' - -import { BoardSchema } from 'lib/schemas' import { RadioInputBgImages } from './RadioInputBgImages' import { RadioInputIcons } from './RadioInputIcons' diff --git a/src/components/dashboard/modals/board/RadioInputBgImages.tsx b/src/features/kanban/board/components/modals/RadioInputBgImages.tsx similarity index 90% rename from src/components/dashboard/modals/board/RadioInputBgImages.tsx rename to src/features/kanban/board/components/modals/RadioInputBgImages.tsx index 9bcd684a..ff685d4f 100644 --- a/src/components/dashboard/modals/board/RadioInputBgImages.tsx +++ b/src/features/kanban/board/components/modals/RadioInputBgImages.tsx @@ -1,15 +1,16 @@ -import type { BoardSchema } from 'lib/schemas' +import type { BoardSchema } from 'features/kanban/board/board.schema' import type { Control } from 'react-hook-form' import { Item, Root } from '@radix-ui/react-radio-group' import { Controller } from 'react-hook-form' +import images from 'features/kanban/board/data/board-bg-images.json' + import { useAppSelector } from 'hooks/redux' import { selectUserTheme } from 'redux/user.slice' import { cn } from 'lib' -import images from 'lib/json/board-bg-images.json' type RadioInputBgImagesProps = { control: Control diff --git a/src/components/dashboard/modals/board/RadioInputIcons.tsx b/src/features/kanban/board/components/modals/RadioInputIcons.tsx similarity index 87% rename from src/components/dashboard/modals/board/RadioInputIcons.tsx rename to src/features/kanban/board/components/modals/RadioInputIcons.tsx index cba6691d..b83652a4 100644 --- a/src/components/dashboard/modals/board/RadioInputIcons.tsx +++ b/src/features/kanban/board/components/modals/RadioInputIcons.tsx @@ -1,10 +1,10 @@ -import type { BoardSchema } from 'lib/schemas' +import type { BoardSchema } from 'features/kanban/board/board.schema' import type { Control } from 'react-hook-form' import { Item, Root } from '@radix-ui/react-radio-group' import { Controller } from 'react-hook-form' -import { icons } from 'constants/icons' +import { ICONS } from 'features/kanban/board/board.constants' type RadioInputIconsProps = { control: Control @@ -20,7 +20,7 @@ export const RadioInputIcons = ({ control }: RadioInputIconsProps) => ( - {icons.map(icon => ( + {ICONS.map(icon => ( `${BoardApiEndpoints.Board}/${boardId}` +} diff --git a/src/features/kanban/board/config/board-cache-keys.config.ts b/src/features/kanban/board/config/board-cache-keys.config.ts new file mode 100644 index 00000000..efa3c8c9 --- /dev/null +++ b/src/features/kanban/board/config/board-cache-keys.config.ts @@ -0,0 +1,7 @@ +export enum BoardCacheKeys { + Boards = 'boards', + Board = 'board', + AddBoard = 'addBoard', + EditBoard = 'editBoard', + DeleteBoard = 'deleteBoard' +} diff --git a/src/features/kanban/board/config/index.ts b/src/features/kanban/board/config/index.ts new file mode 100644 index 00000000..0effb477 --- /dev/null +++ b/src/features/kanban/board/config/index.ts @@ -0,0 +1,2 @@ +export * from './board-api-endpoints.config' +export * from './board-cache-keys.config' diff --git a/src/lib/json/board-bg-images.json b/src/features/kanban/board/data/board-bg-images.json similarity index 100% rename from src/lib/json/board-bg-images.json rename to src/features/kanban/board/data/board-bg-images.json diff --git a/src/hooks/board/index.ts b/src/features/kanban/board/hooks/index.ts similarity index 82% rename from src/hooks/board/index.ts rename to src/features/kanban/board/hooks/index.ts index bd1c93f8..4b5ec9b9 100644 --- a/src/hooks/board/index.ts +++ b/src/features/kanban/board/hooks/index.ts @@ -1,5 +1,6 @@ export * from './useAddBoard' export * from './useDeleteBoard' export * from './useEditBoard' +export * from './useGetAllBoards' export * from './useGetBoardById' export * from './useGetBoardId' diff --git a/src/hooks/board/useAddBoard.ts b/src/features/kanban/board/hooks/useAddBoard.ts similarity index 69% rename from src/hooks/board/useAddBoard.ts rename to src/features/kanban/board/hooks/useAddBoard.ts index d7958148..aa073c48 100644 --- a/src/hooks/board/useAddBoard.ts +++ b/src/features/kanban/board/hooks/useAddBoard.ts @@ -1,15 +1,16 @@ -import type { BoardSchema } from 'lib/schemas' import type { UseFormReset } from 'react-hook-form' +import type { BoardSchema } from '../board.schema' import { useMutation, useQueryClient } from '@tanstack/react-query' import { useModal } from 'react-modal-state' import { useNavigate } from 'react-router-dom' import { toast } from 'sonner' -import { NewBoardModal } from 'components/dashboard/modals' +import { Pages } from 'config' -import { CacheKeys, Pages } from 'config' -import { boardService } from 'services' +import { boardService } from '../board.service' +import { NewBoardModal } from '../components/modals' +import { BoardCacheKeys } from '../config' export const useAddBoard = (reset: UseFormReset) => { const navigate = useNavigate() @@ -19,13 +20,13 @@ export const useAddBoard = (reset: UseFormReset) => { const { close } = useModal(NewBoardModal) return useMutation({ - mutationKey: [CacheKeys.AddBoard], + mutationKey: [BoardCacheKeys.AddBoard], mutationFn: boardService.addNewBoard, onSuccess(data) { close() reset() navigate(`${Pages.Dashboard}/${data.id}`) - queryClient.invalidateQueries({ queryKey: [CacheKeys.Boards] }) + queryClient.invalidateQueries({ queryKey: [BoardCacheKeys.Boards] }) }, onError() { toast.error( diff --git a/src/hooks/board/useDeleteBoard.ts b/src/features/kanban/board/hooks/useDeleteBoard.ts similarity index 64% rename from src/hooks/board/useDeleteBoard.ts rename to src/features/kanban/board/hooks/useDeleteBoard.ts index db806604..4b915f25 100644 --- a/src/hooks/board/useDeleteBoard.ts +++ b/src/features/kanban/board/hooks/useDeleteBoard.ts @@ -1,12 +1,13 @@ -import type { Board } from 'types' +import type { Board } from '../board.types' import { useMutation, useQueryClient } from '@tanstack/react-query' import { useNavigate } from 'react-router-dom' import { toast } from 'sonner' -import { CacheKeys, Pages } from 'config' -import { boardService } from 'services' +import { Pages } from 'config' +import { boardService } from '../board.service' +import { BoardCacheKeys } from '../config' import { useGetBoardId } from './useGetBoardId' export const useDeleteBoard = () => { @@ -17,17 +18,17 @@ export const useDeleteBoard = () => { const navigate = useNavigate() return useMutation({ - mutationKey: [CacheKeys.DeleteBoard], + mutationKey: [BoardCacheKeys.DeleteBoard], mutationFn: () => boardService.deleteBoard(boardId!), onMutate: async () => { - await queryClient.cancelQueries({ queryKey: [CacheKeys.Boards] }) + await queryClient.cancelQueries({ queryKey: [BoardCacheKeys.Boards] }) const previousBoards = queryClient.getQueryData([ - CacheKeys.Boards + BoardCacheKeys.Boards ]) queryClient.setQueryData( - [CacheKeys.Boards], + [BoardCacheKeys.Boards], oldBoards => oldBoards && oldBoards.filter(b => b.id !== boardId) ) @@ -36,13 +37,16 @@ export const useDeleteBoard = () => { return { previousBoards } }, onError: (_, __, context) => { - queryClient.setQueryData([CacheKeys.Boards], context?.previousBoards), + queryClient.setQueryData( + [BoardCacheKeys.Boards], + context?.previousBoards + ), toast.error( 'An error occurred while deleting the board. Our technical team has been notified. Please try again shortly.' ) }, onSettled: () => { - queryClient.invalidateQueries({ queryKey: [CacheKeys.Boards] }) + queryClient.invalidateQueries({ queryKey: [BoardCacheKeys.Boards] }) } }) } diff --git a/src/hooks/board/useEditBoard.ts b/src/features/kanban/board/hooks/useEditBoard.ts similarity index 62% rename from src/hooks/board/useEditBoard.ts rename to src/features/kanban/board/hooks/useEditBoard.ts index c0955aa5..f79a5090 100644 --- a/src/hooks/board/useEditBoard.ts +++ b/src/features/kanban/board/hooks/useEditBoard.ts @@ -1,16 +1,14 @@ -import type { BoardSchema } from 'lib/schemas' import type { UseFormReset } from 'react-hook-form' -import type { Board } from 'types' +import type { BoardSchema } from '../board.schema' +import type { Board } from '../board.types' import { useMutation, useQueryClient } from '@tanstack/react-query' import { useModal } from 'react-modal-state' import { toast } from 'sonner' -import { EditBoardModal } from 'components/dashboard/modals' - -import { CacheKeys } from 'config' -import { boardService } from 'services' - +import { boardService } from '../board.service' +import { EditBoardModal } from '../components/modals' +import { BoardCacheKeys } from '../config' import { useGetBoardId } from './useGetBoardId' export const useEditBoard = (reset: UseFormReset) => { @@ -21,20 +19,20 @@ export const useEditBoard = (reset: UseFormReset) => { const { close } = useModal(EditBoardModal) return useMutation({ - mutationKey: [CacheKeys.EditBoard], + mutationKey: [BoardCacheKeys.EditBoard], mutationFn: (data: BoardSchema) => boardService.editBoard(boardId!, data), onMutate: async ({ title, icon }) => { - await queryClient.cancelQueries({ queryKey: [CacheKeys.Boards] }) + await queryClient.cancelQueries({ queryKey: [BoardCacheKeys.Boards] }) close() reset() const previousBoards = queryClient.getQueryData([ - CacheKeys.Boards + BoardCacheKeys.Boards ]) queryClient.setQueryData( - [CacheKeys.Boards], + [BoardCacheKeys.Boards], oldBoards => oldBoards && oldBoards.map(b => (b.id === boardId ? { ...b, title, icon } : b)) @@ -43,16 +41,19 @@ export const useEditBoard = (reset: UseFormReset) => { return { previousBoards } }, onError: (_, __, context) => { - queryClient.setQueryData([CacheKeys.Boards], context?.previousBoards), + queryClient.setQueryData( + [BoardCacheKeys.Boards], + context?.previousBoards + ), toast.error( 'Failed to update the board. Please try again. If the problem persists, contact support.' ) }, onSettled: data => { - queryClient.invalidateQueries({ queryKey: [CacheKeys.Boards] }) + queryClient.invalidateQueries({ queryKey: [BoardCacheKeys.Boards] }) if (data?.id === boardId) { - queryClient.invalidateQueries({ queryKey: [CacheKeys.Board] }) + queryClient.invalidateQueries({ queryKey: [BoardCacheKeys.Board] }) } } }) diff --git a/src/features/kanban/board/hooks/useGetAllBoards.ts b/src/features/kanban/board/hooks/useGetAllBoards.ts new file mode 100644 index 00000000..63ca19d0 --- /dev/null +++ b/src/features/kanban/board/hooks/useGetAllBoards.ts @@ -0,0 +1,10 @@ +import { useQuery } from '@tanstack/react-query' + +import { boardService } from '../board.service' +import { BoardCacheKeys } from '../config' + +export const useGetAllBoards = () => + useQuery({ + queryKey: [BoardCacheKeys.Boards], + queryFn: boardService.getAllBoards + }) diff --git a/src/hooks/board/useGetBoardById.ts b/src/features/kanban/board/hooks/useGetBoardById.ts similarity index 66% rename from src/hooks/board/useGetBoardById.ts rename to src/features/kanban/board/hooks/useGetBoardById.ts index 0c66e0f5..0cc98bf8 100644 --- a/src/hooks/board/useGetBoardById.ts +++ b/src/features/kanban/board/hooks/useGetBoardById.ts @@ -1,15 +1,14 @@ import { useQuery } from '@tanstack/react-query' -import { CacheKeys } from 'config' -import { boardService } from 'services' - +import { boardService } from '../board.service' +import { BoardCacheKeys } from '../config' import { useGetBoardId } from './useGetBoardId' export const useGetBoardById = () => { const boardId = useGetBoardId() return useQuery({ - queryKey: [CacheKeys.Board, boardId], + queryKey: [BoardCacheKeys.Board, boardId], queryFn: () => boardService.getBoardById(boardId!), enabled: !!boardId }) diff --git a/src/hooks/board/useGetBoardId.ts b/src/features/kanban/board/hooks/useGetBoardId.ts similarity index 100% rename from src/hooks/board/useGetBoardId.ts rename to src/features/kanban/board/hooks/useGetBoardId.ts diff --git a/src/lib/schemas/card.schema.ts b/src/features/kanban/card/card.schema.ts similarity index 61% rename from src/lib/schemas/card.schema.ts rename to src/features/kanban/card/card.schema.ts index 965a5de8..1b1c1466 100644 --- a/src/lib/schemas/card.schema.ts +++ b/src/features/kanban/card/card.schema.ts @@ -1,13 +1,12 @@ import * as v from 'valibot' -import { priorities } from 'constants/priorities' - -import { TitleSchema } from './board.schema' +import { PRIORITIES } from '../shared/constants' +import { TitleSchema } from '../shared/schema' export const CardSchema = v.object({ ...TitleSchema.entries, description: TitleSchema.entries.title, - priority: v.picklist(priorities), + priority: v.picklist(PRIORITIES), deadline: v.date() }) diff --git a/src/features/kanban/card/card.service.ts b/src/features/kanban/card/card.service.ts new file mode 100644 index 00000000..99be4203 --- /dev/null +++ b/src/features/kanban/card/card.service.ts @@ -0,0 +1,24 @@ +import type { UpdateOrderData } from '../shared/types' +import type { CardSchema } from './card.schema' + +import { axiosInstance } from 'api' + +import { CardApiEndpoints } from './config' + +export const cardService = { + async addNewCard(columnId: string, data: CardSchema) { + await axiosInstance.post(CardApiEndpoints.CardColumnById(columnId), data) + }, + + async editCard(cardId: string, data: CardSchema) { + await axiosInstance.put(CardApiEndpoints.CardById(cardId), data) + }, + + async deleteCard(cardId: string) { + await axiosInstance.delete(CardApiEndpoints.CardById(cardId)) + }, + + async updateCardsOrder(columnId: string, data: UpdateOrderData) { + await axiosInstance.patch(CardApiEndpoints.CardOrder(columnId), data) + } +} diff --git a/src/features/kanban/card/card.types.ts b/src/features/kanban/card/card.types.ts new file mode 100644 index 00000000..d55a81e7 --- /dev/null +++ b/src/features/kanban/card/card.types.ts @@ -0,0 +1,11 @@ +import type { Priority } from '../shared/constants' + +export type Card = { + id: string + title: string + order: number + columnId: string + description: string + priority: Priority + deadline: Date +} diff --git a/src/components/dashboard/board/cards/BoardCard.tsx b/src/features/kanban/card/components/BoardCard.tsx similarity index 77% rename from src/components/dashboard/board/cards/BoardCard.tsx rename to src/features/kanban/card/components/BoardCard.tsx index d4e5dd5a..621fd30a 100644 --- a/src/components/dashboard/board/cards/BoardCard.tsx +++ b/src/features/kanban/card/components/BoardCard.tsx @@ -1,31 +1,20 @@ -import type { Card } from 'types' +import type { Card } from '../card.types' -import { useSortable } from '@dnd-kit/sortable' -import { CSS } from '@dnd-kit/utilities' +import { useKanbanSortable } from 'features/kanban/dnd/hooks' -import { cn, getPriorityColor } from 'lib' +import { cn } from 'lib' +import { getPriorityColor } from '../utils' import { BoardCardActions } from './BoardCardActions' import { BoardCardDeadline } from './BoardCardDeadline' import { BoardCardPriority } from './BoardCardPriority' export const BoardCard = ({ card }: { card: Card }) => { - const { - setNodeRef, - attributes, - listeners, - transition, - transform, - isDragging - } = useSortable({ - id: card.id, - data: { type: 'card', card } - }) - - const style = { - transition, - transform: CSS.Transform.toString(transform) - } + const { style, setNodeRef, attributes, listeners, isDragging } = + useKanbanSortable({ + id: card.id, + data: { type: 'card', card } + }) return isDragging ? (
{ const { open } = useModal(EditCardModal) diff --git a/src/components/dashboard/board/cards/BoardCardDeadline.tsx b/src/features/kanban/card/components/BoardCardDeadline.tsx similarity index 89% rename from src/components/dashboard/board/cards/BoardCardDeadline.tsx rename to src/features/kanban/card/components/BoardCardDeadline.tsx index 7ce5f150..87467fc3 100644 --- a/src/components/dashboard/board/cards/BoardCardDeadline.tsx +++ b/src/features/kanban/card/components/BoardCardDeadline.tsx @@ -1,4 +1,4 @@ -import type { Card } from 'types' +import type { Card } from '../card.types' import { format } from 'date-fns' diff --git a/src/components/dashboard/board/cards/BoardCardPriority.tsx b/src/features/kanban/card/components/BoardCardPriority.tsx similarity index 76% rename from src/components/dashboard/board/cards/BoardCardPriority.tsx rename to src/features/kanban/card/components/BoardCardPriority.tsx index dce2971c..4244721e 100644 --- a/src/components/dashboard/board/cards/BoardCardPriority.tsx +++ b/src/features/kanban/card/components/BoardCardPriority.tsx @@ -1,6 +1,8 @@ -import type { Priority } from 'constants/priorities' +import type { Priority } from 'features/kanban/shared/constants' -import { cn, getPriorityColor } from 'lib' +import { cn } from 'lib' + +import { getPriorityColor } from '../utils' type BoardCardPriorityProps = { priority: Priority diff --git a/src/components/dashboard/modals/card/EditCard.tsx b/src/features/kanban/card/components/modals/EditCardModal.tsx similarity index 86% rename from src/components/dashboard/modals/card/EditCard.tsx rename to src/features/kanban/card/components/modals/EditCardModal.tsx index 67c4ecca..c330eb30 100644 --- a/src/components/dashboard/modals/card/EditCard.tsx +++ b/src/features/kanban/card/components/modals/EditCardModal.tsx @@ -1,16 +1,16 @@ -import type { Card } from 'types' +import type { Card } from 'features/kanban/card/card.types' import { useEffect } from 'react' import { useModalInstance } from 'react-modal-state' import { keyof } from 'valibot' -import { Button, DatePicker, Field, Modal } from 'components/ui' +import { CardSchema } from 'features/kanban/card/card.schema' +import { useEditCard } from 'features/kanban/card/hooks' +import { Button, Field, Modal } from 'components/ui' import { useAppForm } from 'hooks' -import { useEditCard } from 'hooks/card' - -import { CardSchema } from 'lib/schemas' +import { DatePicker } from '../ui' import { ModalDescription } from './ModalDescription' import { ModalPriorities } from './ModalPriorities' diff --git a/src/components/dashboard/modals/card/ModalDescription.tsx b/src/features/kanban/card/components/modals/ModalDescription.tsx similarity index 92% rename from src/components/dashboard/modals/card/ModalDescription.tsx rename to src/features/kanban/card/components/modals/ModalDescription.tsx index 71f1fe0d..b49c7fa3 100644 --- a/src/components/dashboard/modals/card/ModalDescription.tsx +++ b/src/features/kanban/card/components/modals/ModalDescription.tsx @@ -1,4 +1,4 @@ -import type { CardSchema } from 'lib/schemas' +import type { CardSchema } from 'features/kanban/card/card.schema' import type { FieldErrors } from 'react-hook-form' import { forwardRef } from 'react' diff --git a/src/components/dashboard/modals/card/ModalPriorities.tsx b/src/features/kanban/card/components/modals/ModalPriorities.tsx similarity index 80% rename from src/components/dashboard/modals/card/ModalPriorities.tsx rename to src/features/kanban/card/components/modals/ModalPriorities.tsx index a6ddebc2..ed2d5328 100644 --- a/src/components/dashboard/modals/card/ModalPriorities.tsx +++ b/src/features/kanban/card/components/modals/ModalPriorities.tsx @@ -1,12 +1,11 @@ -import type { CardSchema } from 'lib/schemas' +import type { CardSchema } from 'features/kanban/card/card.schema' import type { Control } from 'react-hook-form' import { Root } from '@radix-ui/react-radio-group' import { Controller } from 'react-hook-form' -import { RadioInput } from 'components/ui' - -import { priorities } from 'constants/priorities' +import { RadioInput } from 'features/kanban/shared/components' +import { PRIORITIES } from 'features/kanban/shared/constants' type ModalPrioritiesProps = { control: Control @@ -23,7 +22,7 @@ export const ModalPriorities = ({ control }: ModalPrioritiesProps) => ( - {priorities.map(priority => ( + {PRIORITIES.map(priority => ( `${CardApiEndpoints.Card}/${cardId}`, + CardColumnById: (columnId: string) => `${CardApiEndpoints.Card}/${columnId}`, + CardOrder: (columnId: string) => + `${CardApiEndpoints.CardColumnById(columnId)}/order` +} diff --git a/src/features/kanban/card/config/card-cache-keys.config.ts b/src/features/kanban/card/config/card-cache-keys.config.ts new file mode 100644 index 00000000..93508d5b --- /dev/null +++ b/src/features/kanban/card/config/card-cache-keys.config.ts @@ -0,0 +1,6 @@ +export enum CardCacheKeys { + AddCard = 'addCard', + EditCard = 'editCard', + DeleteCard = 'deleteCard', + UpdateCardsOrder = 'updateCardsOrder' +} diff --git a/src/features/kanban/card/config/index.ts b/src/features/kanban/card/config/index.ts new file mode 100644 index 00000000..6e35e2b0 --- /dev/null +++ b/src/features/kanban/card/config/index.ts @@ -0,0 +1,2 @@ +export * from './card-api-endpoints.config' +export * from './card-cache-keys.config' diff --git a/src/hooks/card/index.ts b/src/features/kanban/card/hooks/index.ts similarity index 100% rename from src/hooks/card/index.ts rename to src/features/kanban/card/hooks/index.ts diff --git a/src/hooks/card/useAddCard.ts b/src/features/kanban/card/hooks/useAddCard.ts similarity index 66% rename from src/hooks/card/useAddCard.ts rename to src/features/kanban/card/hooks/useAddCard.ts index 9ac7d256..af827dee 100644 --- a/src/hooks/card/useAddCard.ts +++ b/src/features/kanban/card/hooks/useAddCard.ts @@ -1,14 +1,15 @@ -import type { CardSchema } from 'lib/schemas' import type { UseFormReset } from 'react-hook-form' +import type { CardSchema } from '../card.schema' import { useMutation, useQueryClient } from '@tanstack/react-query' import { useModal, useModalInstance } from 'react-modal-state' import { toast } from 'sonner' -import { NewCardModal } from 'components/dashboard/modals' +import { BoardCacheKeys } from 'features/kanban/board/config' -import { CacheKeys } from 'config' -import { cardService } from 'services' +import { cardService } from '../card.service' +import { NewCardModal } from '../components/modals' +import { CardCacheKeys } from '../config' export const useAddCard = (reset: UseFormReset) => { const queryClient = useQueryClient() @@ -18,10 +19,10 @@ export const useAddCard = (reset: UseFormReset) => { const { close } = useModal(NewCardModal) return useMutation({ - mutationKey: [CacheKeys.AddCard], + mutationKey: [CardCacheKeys.AddCard], mutationFn: (data: CardSchema) => cardService.addNewCard(column, data), onSuccess() { - queryClient.invalidateQueries({ queryKey: [CacheKeys.Board] }) + queryClient.invalidateQueries({ queryKey: [BoardCacheKeys.Board] }) close() reset() }, diff --git a/src/hooks/card/useCardDragHandlers.ts b/src/features/kanban/card/hooks/useCardDragHandlers.ts similarity index 92% rename from src/hooks/card/useCardDragHandlers.ts rename to src/features/kanban/card/hooks/useCardDragHandlers.ts index 47a35ddf..bdece50b 100644 --- a/src/hooks/card/useCardDragHandlers.ts +++ b/src/features/kanban/card/hooks/useCardDragHandlers.ts @@ -1,11 +1,11 @@ import type { DragEndEvent, DragOverEvent, DragStartEvent } from '@dnd-kit/core' -import type { DragAndDropContext } from 'context/dnd.context' +import type { DragAndDropContext } from 'features/kanban/dnd/dnd.context' import type { Dispatch, SetStateAction } from 'react' -import type { Card } from 'types' +import type { Card } from '../card.types' import { arrayMove } from '@dnd-kit/sortable' -import { findIndexById } from 'lib' +import { findIndexById } from 'features/kanban/dnd/utils' import { useUpdateCardsOrder } from './useUpdateCardsOrder' diff --git a/src/hooks/card/useCardFilters.ts b/src/features/kanban/card/hooks/useCardFilters.ts similarity index 77% rename from src/hooks/card/useCardFilters.ts rename to src/features/kanban/card/hooks/useCardFilters.ts index d2063fc2..8955bdbd 100644 --- a/src/hooks/card/useCardFilters.ts +++ b/src/features/kanban/card/hooks/useCardFilters.ts @@ -1,5 +1,4 @@ -import type { Deadline } from 'constants/deadlines' -import type { Priority } from 'constants/priorities' +import type { Deadline, Priority } from 'features/kanban/shared/constants' import { useSearchParams } from 'react-router-dom' diff --git a/src/hooks/card/useDeleteCard.ts b/src/features/kanban/card/hooks/useDeleteCard.ts similarity index 56% rename from src/hooks/card/useDeleteCard.ts rename to src/features/kanban/card/hooks/useDeleteCard.ts index 71b9f983..5400c51f 100644 --- a/src/hooks/card/useDeleteCard.ts +++ b/src/features/kanban/card/hooks/useDeleteCard.ts @@ -1,8 +1,10 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' -import { useDragAndDrop } from 'context/dnd.context' -import { CacheKeys } from 'config' -import { cardService } from 'services' +import { BoardCacheKeys } from 'features/kanban/board/config' +import { useDragAndDrop } from 'features/kanban/dnd/hooks' + +import { cardService } from '../card.service' +import { CardCacheKeys } from '../config' export const useDeleteCard = () => { const queryClient = useQueryClient() @@ -10,13 +12,13 @@ export const useDeleteCard = () => { const { setCards } = useDragAndDrop() return useMutation({ - mutationKey: [CacheKeys.DeleteCard], + mutationKey: [CardCacheKeys.DeleteCard], mutationFn: (cardId: string) => cardService.deleteCard(cardId), onMutate: async cardId => { setCards(prevCards => prevCards?.filter(c => c.id !== cardId)) }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: [CacheKeys.Board] }) + queryClient.invalidateQueries({ queryKey: [BoardCacheKeys.Board] }) } }) } diff --git a/src/hooks/card/useEditCard.ts b/src/features/kanban/card/hooks/useEditCard.ts similarity index 66% rename from src/hooks/card/useEditCard.ts rename to src/features/kanban/card/hooks/useEditCard.ts index 18b7a299..e839b9c0 100644 --- a/src/hooks/card/useEditCard.ts +++ b/src/features/kanban/card/hooks/useEditCard.ts @@ -1,17 +1,17 @@ -import type { CardSchema } from 'lib/schemas' +import type { Board } from 'features/kanban/board/board.types' import type { UseFormReset } from 'react-hook-form' -import type { Board } from 'types' +import type { CardSchema } from '../card.schema' import { useMutation, useQueryClient } from '@tanstack/react-query' import { useModal } from 'react-modal-state' import { toast } from 'sonner' -import { EditCardModal } from 'components/dashboard/modals' +import { BoardCacheKeys } from 'features/kanban/board/config' +import { useGetBoardId } from 'features/kanban/board/hooks' -import { useGetBoardId } from 'hooks/board' - -import { CacheKeys } from 'config' -import { cardService } from 'services' +import { cardService } from '../card.service' +import { EditCardModal } from '../components/modals' +import { CardCacheKeys } from '../config' export const useEditCard = (reset: UseFormReset) => { const queryClient = useQueryClient() @@ -21,7 +21,7 @@ export const useEditCard = (reset: UseFormReset) => { const { close } = useModal(EditCardModal) return useMutation({ - mutationKey: [CacheKeys.EditCard], + mutationKey: [CardCacheKeys.EditCard], mutationFn: ({ cardId, cardData @@ -30,18 +30,20 @@ export const useEditCard = (reset: UseFormReset) => { cardData: CardSchema }) => cardService.editCard(cardId, cardData), onMutate: async ({ cardId, cardData }) => { - await queryClient.cancelQueries({ queryKey: [CacheKeys.Board, boardId] }) + await queryClient.cancelQueries({ + queryKey: [BoardCacheKeys.Board, boardId] + }) close() reset() const previousBoard = queryClient.getQueryData([ - CacheKeys.Board, + BoardCacheKeys.Board, boardId ]) queryClient.setQueryData( - [CacheKeys.Board, boardId], + [BoardCacheKeys.Board, boardId], oldBoard => oldBoard && { ...oldBoard, @@ -58,7 +60,7 @@ export const useEditCard = (reset: UseFormReset) => { }, onError: (_, __, context) => { queryClient.setQueryData( - [CacheKeys.Board, boardId], + [BoardCacheKeys.Board, boardId], context?.previousBoard ), toast.error( @@ -66,7 +68,9 @@ export const useEditCard = (reset: UseFormReset) => { ) }, onSettled: () => { - queryClient.invalidateQueries({ queryKey: [CacheKeys.Board, boardId] }) + queryClient.invalidateQueries({ + queryKey: [BoardCacheKeys.Board, boardId] + }) } }) } diff --git a/src/hooks/card/useUpdateCardsOrder.ts b/src/features/kanban/card/hooks/useUpdateCardsOrder.ts similarity index 60% rename from src/hooks/card/useUpdateCardsOrder.ts rename to src/features/kanban/card/hooks/useUpdateCardsOrder.ts index 9233c290..1c13195c 100644 --- a/src/hooks/card/useUpdateCardsOrder.ts +++ b/src/features/kanban/card/hooks/useUpdateCardsOrder.ts @@ -1,20 +1,22 @@ -import type { UpdateOrderData } from 'types' +import type { UpdateOrderData } from 'features/kanban/shared/types' import { useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' -import { CacheKeys } from 'config' -import { cardService } from 'services' +import { BoardCacheKeys } from 'features/kanban/board/config' + +import { cardService } from '../card.service' +import { CardCacheKeys } from '../config' export const useUpdateCardsOrder = () => { const queryClient = useQueryClient() return useMutation({ - mutationKey: [CacheKeys.UpdateCardsOrder], + mutationKey: [CardCacheKeys.UpdateCardsOrder], mutationFn: ({ columnId, ids }: UpdateOrderData & { columnId: string }) => cardService.updateCardsOrder(columnId, { ids }), onSuccess() { - queryClient.invalidateQueries({ queryKey: [CacheKeys.Board] }) + queryClient.invalidateQueries({ queryKey: [BoardCacheKeys.Board] }) }, onError() { toast.error( diff --git a/src/features/kanban/card/utils/formatTodayDate.ts b/src/features/kanban/card/utils/formatTodayDate.ts new file mode 100644 index 00000000..17842575 --- /dev/null +++ b/src/features/kanban/card/utils/formatTodayDate.ts @@ -0,0 +1,6 @@ +import { format, isToday } from 'date-fns' + +export const formatTodayDate = (date: Date) => + isToday(date) + ? `Today, ${format(date, 'MMMM d')}` + : format(date, 'dd/MM/yyyy') diff --git a/src/features/kanban/card/utils/getPriorityColor.ts b/src/features/kanban/card/utils/getPriorityColor.ts new file mode 100644 index 00000000..26c0077d --- /dev/null +++ b/src/features/kanban/card/utils/getPriorityColor.ts @@ -0,0 +1,12 @@ +import type { Priority } from 'features/kanban/shared/constants' + +export const getPriorityColor = (priority: Priority) => { + const priorityColors: { [key: string]: string } = { + Low: 'bg-priority-low', + Medium: 'bg-priority-medium', + High: 'bg-brand', + default: 'bg-black/30 dark:bg-white/30' + } + + return priorityColors[priority] || priorityColors.default +} diff --git a/src/features/kanban/card/utils/index.ts b/src/features/kanban/card/utils/index.ts new file mode 100644 index 00000000..d9fdd582 --- /dev/null +++ b/src/features/kanban/card/utils/index.ts @@ -0,0 +1,2 @@ +export * from './formatTodayDate' +export * from './getPriorityColor' diff --git a/src/constants/titles.ts b/src/features/kanban/column/column.constants.ts similarity index 100% rename from src/constants/titles.ts rename to src/features/kanban/column/column.constants.ts diff --git a/src/features/kanban/column/column.service.ts b/src/features/kanban/column/column.service.ts new file mode 100644 index 00000000..26f19bb5 --- /dev/null +++ b/src/features/kanban/column/column.service.ts @@ -0,0 +1,24 @@ +import type { UpdateOrderData } from '../shared/types' +import type { ColumnTitle } from './column.types' + +import { axiosInstance } from 'api' + +import { ColumnApiEndpoints } from './config' + +export const columnService = { + async addNewColumn(boardId: string, data: ColumnTitle) { + await axiosInstance.post(ColumnApiEndpoints.ColumnBoardById(boardId), data) + }, + + async editColumn(columnId: string, data: ColumnTitle) { + await axiosInstance.put(ColumnApiEndpoints.ColumnById(columnId), data) + }, + + async deleteColumn(columnId: string) { + await axiosInstance.delete(ColumnApiEndpoints.ColumnById(columnId)) + }, + + async updateColumnsOrder(boardId: string, data: UpdateOrderData) { + await axiosInstance.patch(ColumnApiEndpoints.ColumnOrder(boardId), data) + } +} diff --git a/src/features/kanban/column/column.types.ts b/src/features/kanban/column/column.types.ts new file mode 100644 index 00000000..efdc32bc --- /dev/null +++ b/src/features/kanban/column/column.types.ts @@ -0,0 +1,11 @@ +import type { Card } from '../card/card.types' + +export type Column = { + id: string + title: string + order: number + boardId: string + cards: Card[] +} + +export type ColumnTitle = Pick diff --git a/src/components/dashboard/board/columns/BoardAddColumnBtn.tsx b/src/features/kanban/column/components/BoardAddColumnBtn.tsx similarity index 93% rename from src/components/dashboard/board/columns/BoardAddColumnBtn.tsx rename to src/features/kanban/column/components/BoardAddColumnBtn.tsx index 1580ea78..55f6643e 100644 --- a/src/components/dashboard/board/columns/BoardAddColumnBtn.tsx +++ b/src/features/kanban/column/components/BoardAddColumnBtn.tsx @@ -1,6 +1,6 @@ -import { Button, Loader } from 'components/ui' +import { useAddColumn } from 'features/kanban/column/hooks' -import { useAddColumn } from 'hooks/column' +import { Button, Loader } from 'components/ui' export const BoardAddColumnBtn = () => { const { mutate, isPending } = useAddColumn() diff --git a/src/components/dashboard/board/columns/BoardColumnsActions.tsx b/src/features/kanban/column/components/BoardColumnsActions.tsx similarity index 90% rename from src/components/dashboard/board/columns/BoardColumnsActions.tsx rename to src/features/kanban/column/components/BoardColumnsActions.tsx index 7bc37d46..5fe27399 100644 --- a/src/components/dashboard/board/columns/BoardColumnsActions.tsx +++ b/src/features/kanban/column/components/BoardColumnsActions.tsx @@ -1,20 +1,20 @@ import type { onSaveProps } from 'react-edit-text' -import type { Column } from 'types' +import type { Column } from '../column.types' import { useState } from 'react' import { EditText } from 'react-edit-text' import { toast } from 'sonner' +import { useDeleteColumn, useEditColumn } from 'features/kanban/column/hooks' + import { Button } from 'components/ui' -import { useDeleteColumn, useEditColumn } from 'hooks/column' +import { cn } from 'lib' import { DEFAULT_COLUMN_TITLE, REQUIRED_COLUMN_TITLE_LENGTH -} from 'constants/titles' - -import { cn } from 'lib' +} from '../column.constants' export const BoardColumnsActions = ({ column }: { column: Column }) => { const [columnTitle, setColumnTitle] = useState(column.title) diff --git a/src/components/dashboard/board/columns/BoardColumnsItem.tsx b/src/features/kanban/column/components/BoardColumnsItem.tsx similarity index 76% rename from src/components/dashboard/board/columns/BoardColumnsItem.tsx rename to src/features/kanban/column/components/BoardColumnsItem.tsx index 714e3790..8d2ac034 100644 --- a/src/components/dashboard/board/columns/BoardColumnsItem.tsx +++ b/src/features/kanban/column/components/BoardColumnsItem.tsx @@ -1,25 +1,21 @@ -import type { Card, Column } from 'types' +import type { Card } from 'features/kanban/card/card.types' +import type { Column } from '../column.types' -import { - SortableContext, - useSortable, - verticalListSortingStrategy -} from '@dnd-kit/sortable' -import { CSS } from '@dnd-kit/utilities' +import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable' import * as ScrollArea from '@radix-ui/react-scroll-area' -import { useDragAndDrop } from 'context/dnd.context' import { useModal } from 'react-modal-state' -import { NewCardModal } from 'components/dashboard/modals' -import { Button, Scrollbar } from 'components/ui' +import { BoardCard } from 'features/kanban/card/components/BoardCard' +import { NewCardModal } from 'features/kanban/card/components/modals' +import { useCardFilters } from 'features/kanban/card/hooks' +import { useDragAndDrop, useKanbanSortable } from 'features/kanban/dnd/hooks' +import { getFilteredCards } from 'features/kanban/filters/utils' +import { Button, Scrollbar } from 'components/ui' import { useTabletAndBelowMediaQuery } from 'hooks' -import { useCardFilters } from 'hooks/card' import { cn } from 'lib' -import { getFilteredCards } from 'lib/filters' -import { BoardCard } from '../cards/BoardCard' import { BoardColumnsActions } from './BoardColumnsActions' type BoardColumnsItemProps = { @@ -46,22 +42,11 @@ export const BoardColumnsItem = ({ deadline: cardDeadline }) - const { - setNodeRef, - attributes, - listeners, - transition, - transform, - isDragging - } = useSortable({ - id: column.id, - data: { type: 'column', column } - }) - - const style = { - transition, - transform: CSS.Transform.toString(transform) - } + const { style, setNodeRef, attributes, listeners, isDragging } = + useKanbanSortable({ + id: column.id, + data: { type: 'column', column } + }) return isDragging ? (
`${ColumnApiEndpoints.Column}/${columnId}`, + ColumnBoardById: (boardId: string) => + `${ColumnApiEndpoints.Column}/${boardId}`, + ColumnOrder: (boardId: string) => + `${ColumnApiEndpoints.ColumnBoardById(boardId)}/order` +} diff --git a/src/features/kanban/column/config/column-cache-keys.config.ts b/src/features/kanban/column/config/column-cache-keys.config.ts new file mode 100644 index 00000000..fbeb8836 --- /dev/null +++ b/src/features/kanban/column/config/column-cache-keys.config.ts @@ -0,0 +1,6 @@ +export enum ColumnCacheKeys { + AddColumn = 'addColumn', + EditColumn = 'editColumn', + DeleteColumn = 'deleteColumn', + UpdateColumnsOrder = 'updateColumnsOrder' +} diff --git a/src/features/kanban/column/config/index.ts b/src/features/kanban/column/config/index.ts new file mode 100644 index 00000000..9f571780 --- /dev/null +++ b/src/features/kanban/column/config/index.ts @@ -0,0 +1,2 @@ +export * from './column-api-endpoints.config' +export * from './column-cache-keys.config' diff --git a/src/hooks/column/index.ts b/src/features/kanban/column/hooks/index.ts similarity index 100% rename from src/hooks/column/index.ts rename to src/features/kanban/column/hooks/index.ts diff --git a/src/hooks/column/useAddColumn.ts b/src/features/kanban/column/hooks/useAddColumn.ts similarity index 57% rename from src/hooks/column/useAddColumn.ts rename to src/features/kanban/column/hooks/useAddColumn.ts index 63452b93..74b3f8ed 100644 --- a/src/hooks/column/useAddColumn.ts +++ b/src/features/kanban/column/hooks/useAddColumn.ts @@ -1,11 +1,12 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' -import { useGetBoardId } from 'hooks/board' +import { BoardCacheKeys } from 'features/kanban/board/config' +import { useGetBoardId } from 'features/kanban/board/hooks' -import { CacheKeys } from 'config' -import { DEFAULT_COLUMN_TITLE } from 'constants/titles' -import { columnService } from 'services' +import { DEFAULT_COLUMN_TITLE } from '../column.constants' +import { columnService } from '../column.service' +import { ColumnCacheKeys } from '../config' export const useAddColumn = () => { const queryClient = useQueryClient() @@ -13,11 +14,11 @@ export const useAddColumn = () => { const boardId = useGetBoardId() return useMutation({ - mutationKey: [CacheKeys.AddColumn], + mutationKey: [ColumnCacheKeys.AddColumn], mutationFn: () => columnService.addNewColumn(boardId!, { title: DEFAULT_COLUMN_TITLE }), onSuccess() { - queryClient.invalidateQueries({ queryKey: [CacheKeys.Board] }) + queryClient.invalidateQueries({ queryKey: [BoardCacheKeys.Board] }) }, onError() { toast.error( diff --git a/src/hooks/column/useColumnDragHandlers.ts b/src/features/kanban/column/hooks/useColumnDragHandlers.ts similarity index 87% rename from src/hooks/column/useColumnDragHandlers.ts rename to src/features/kanban/column/hooks/useColumnDragHandlers.ts index 4b65ebd3..ca2beb31 100644 --- a/src/hooks/column/useColumnDragHandlers.ts +++ b/src/features/kanban/column/hooks/useColumnDragHandlers.ts @@ -1,11 +1,11 @@ import type { DragEndEvent, DragStartEvent } from '@dnd-kit/core' -import type { DragAndDropContext } from 'context/dnd.context' +import type { DragAndDropContext } from 'features/kanban/dnd/dnd.context' import type { Dispatch, SetStateAction } from 'react' -import type { Column } from 'types' +import type { Column } from '../column.types' import { arrayMove } from '@dnd-kit/sortable' -import { findIndexById } from 'lib' +import { findIndexById } from 'features/kanban/dnd/utils' import { useUpdateColumnsOrder } from './useUpdateColumnsOrder' diff --git a/src/hooks/column/useDeleteColumn.ts b/src/features/kanban/column/hooks/useDeleteColumn.ts similarity index 57% rename from src/hooks/column/useDeleteColumn.ts rename to src/features/kanban/column/hooks/useDeleteColumn.ts index a11e62d0..82de46b9 100644 --- a/src/hooks/column/useDeleteColumn.ts +++ b/src/features/kanban/column/hooks/useDeleteColumn.ts @@ -1,10 +1,11 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' -import { useDragAndDrop } from 'context/dnd.context' -import { useGetBoardId } from 'hooks/board' +import { BoardCacheKeys } from 'features/kanban/board/config' +import { useGetBoardId } from 'features/kanban/board/hooks' +import { useDragAndDrop } from 'features/kanban/dnd/hooks' -import { CacheKeys } from 'config' -import { columnService } from 'services' +import { columnService } from '../column.service' +import { ColumnCacheKeys } from '../config' export const useDeleteColumn = () => { const queryClient = useQueryClient() @@ -14,14 +15,16 @@ export const useDeleteColumn = () => { const boardId = useGetBoardId() return useMutation({ - mutationKey: [CacheKeys.DeleteColumn], + mutationKey: [ColumnCacheKeys.DeleteColumn], mutationFn: (columnId: string) => columnService.deleteColumn(columnId), onMutate: async columnId => { setColumns(prevColumns => prevColumns?.filter(c => c.id !== columnId)) setCards(prevCards => prevCards?.filter(c => c.columnId !== columnId)) }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: [CacheKeys.Board, boardId] }) + queryClient.invalidateQueries({ + queryKey: [BoardCacheKeys.Board, boardId] + }) } }) } diff --git a/src/hooks/column/useEditColumn.ts b/src/features/kanban/column/hooks/useEditColumn.ts similarity index 61% rename from src/hooks/column/useEditColumn.ts rename to src/features/kanban/column/hooks/useEditColumn.ts index cca7999d..fbd2d748 100644 --- a/src/hooks/column/useEditColumn.ts +++ b/src/features/kanban/column/hooks/useEditColumn.ts @@ -1,20 +1,22 @@ -import type { ColumnTitle } from 'types' +import type { ColumnTitle } from '../column.types' import { useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' -import { CacheKeys } from 'config' -import { columnService } from 'services' +import { BoardCacheKeys } from 'features/kanban/board/config' + +import { columnService } from '../column.service' +import { ColumnCacheKeys } from '../config' export const useEditColumn = () => { const queryClient = useQueryClient() return useMutation({ - mutationKey: [CacheKeys.EditColumn], + mutationKey: [ColumnCacheKeys.EditColumn], mutationFn: ({ columnId, data }: { columnId: string; data: ColumnTitle }) => columnService.editColumn(columnId, data), onSuccess() { - queryClient.invalidateQueries({ queryKey: [CacheKeys.Board] }) + queryClient.invalidateQueries({ queryKey: [BoardCacheKeys.Board] }) }, onError() { toast.error( diff --git a/src/hooks/column/useUpdateColumnsOrder.ts b/src/features/kanban/column/hooks/useUpdateColumnsOrder.ts similarity index 56% rename from src/hooks/column/useUpdateColumnsOrder.ts rename to src/features/kanban/column/hooks/useUpdateColumnsOrder.ts index 33a0053f..3f488f34 100644 --- a/src/hooks/column/useUpdateColumnsOrder.ts +++ b/src/features/kanban/column/hooks/useUpdateColumnsOrder.ts @@ -1,12 +1,13 @@ -import type { UpdateOrderData } from 'types' +import type { UpdateOrderData } from 'features/kanban/shared/types' import { useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' -import { useGetBoardId } from 'hooks/board' +import { BoardCacheKeys } from 'features/kanban/board/config' +import { useGetBoardId } from 'features/kanban/board/hooks' -import { CacheKeys } from 'config' -import { columnService } from 'services' +import { columnService } from '../column.service' +import { ColumnCacheKeys } from '../config' export const useUpdateColumnsOrder = () => { const queryClient = useQueryClient() @@ -14,11 +15,11 @@ export const useUpdateColumnsOrder = () => { const boardId = useGetBoardId() return useMutation({ - mutationKey: [CacheKeys.UpdateColumnsOrder], + mutationKey: [ColumnCacheKeys.UpdateColumnsOrder], mutationFn: ({ ids }: UpdateOrderData) => columnService.updateColumnsOrder(boardId!, { ids }), onSuccess() { - queryClient.invalidateQueries({ queryKey: [CacheKeys.Board] }) + queryClient.invalidateQueries({ queryKey: [BoardCacheKeys.Board] }) }, onError() { toast.error( diff --git a/src/context/dnd.context.tsx b/src/features/kanban/dnd/dnd.context.tsx similarity index 81% rename from src/context/dnd.context.tsx rename to src/features/kanban/dnd/dnd.context.tsx index 847cc34c..5a0c6e49 100644 --- a/src/context/dnd.context.tsx +++ b/src/features/kanban/dnd/dnd.context.tsx @@ -1,8 +1,9 @@ import type { DragEndEvent, DragOverEvent, DragStartEvent } from '@dnd-kit/core' +import type { Card } from 'features/kanban/card/card.types' +import type { Column } from 'features/kanban/column/column.types' import type { Dispatch, ReactNode, SetStateAction } from 'react' -import type { Card, Column } from 'types' -import { createContext, useContext, useEffect, useMemo, useState } from 'react' +import { createContext, useEffect, useMemo, useState } from 'react' import { DndContext, DragOverlay, @@ -13,13 +14,12 @@ import { } from '@dnd-kit/core' import { createPortal } from 'react-dom' -import { BoardCard } from 'components/dashboard/board/cards/BoardCard' -import { BoardColumnsItem } from 'components/dashboard/board/columns/BoardColumnsItem' +import { BoardCard } from 'features/kanban/card/components/BoardCard' +import { useCardDragHandlers } from 'features/kanban/card/hooks' +import { BoardColumnsItem } from 'features/kanban/column/components/BoardColumnsItem' +import { useColumnDragHandlers } from 'features/kanban/column/hooks' -import { useCardDragHandlers } from 'hooks/card' -import { useColumnDragHandlers } from 'hooks/column' - -import { collisionDetectionAlgorithm } from 'lib' +import { collisionDetectionAlgorithm } from './utils' export type DragAndDropContext = { setColumns: Dispatch> @@ -35,7 +35,7 @@ type DragAndDropProviderProps = { initialColumns: Column[] | undefined } -const DragAndDropContext = createContext(null) +export const DragAndDropContext = createContext(null) export const DragAndDropProvider = ({ children, @@ -118,13 +118,3 @@ export const DragAndDropProvider = ({ ) } - -export const useDragAndDrop = () => { - const context = useContext(DragAndDropContext) - - if (!context) { - throw new Error('useDragAndDrop must be used within a DragAndDropProvider') - } - - return context -} diff --git a/src/features/kanban/dnd/hooks/index.ts b/src/features/kanban/dnd/hooks/index.ts new file mode 100644 index 00000000..6518cd4f --- /dev/null +++ b/src/features/kanban/dnd/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './useDragAndDrop' +export * from './useKanbanSortable' diff --git a/src/features/kanban/dnd/hooks/useDragAndDrop.ts b/src/features/kanban/dnd/hooks/useDragAndDrop.ts new file mode 100644 index 00000000..a1d2a48f --- /dev/null +++ b/src/features/kanban/dnd/hooks/useDragAndDrop.ts @@ -0,0 +1,13 @@ +import { useContext } from 'react' + +import { DragAndDropContext } from '../dnd.context' + +export const useDragAndDrop = () => { + const context = useContext(DragAndDropContext) + + if (!context) { + throw new Error('useDragAndDrop must be used within a DragAndDropProvider') + } + + return context +} diff --git a/src/features/kanban/dnd/hooks/useKanbanSortable.ts b/src/features/kanban/dnd/hooks/useKanbanSortable.ts new file mode 100644 index 00000000..fea414c3 --- /dev/null +++ b/src/features/kanban/dnd/hooks/useKanbanSortable.ts @@ -0,0 +1,15 @@ +import type { UseDraggableArguments } from '@dnd-kit/core' + +import { useSortable } from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' + +export const useKanbanSortable = (props: UseDraggableArguments) => { + const { transition, transform, ...sortable } = useSortable(props) + + const style = { + transition, + transform: CSS.Transform.toString(transform) + } + + return { ...sortable, style } +} diff --git a/src/features/kanban/dnd/utils/collisionDetectionAlgorithm.ts b/src/features/kanban/dnd/utils/collisionDetectionAlgorithm.ts new file mode 100644 index 00000000..ac692f89 --- /dev/null +++ b/src/features/kanban/dnd/utils/collisionDetectionAlgorithm.ts @@ -0,0 +1,21 @@ +import type { Active, ClientRect, DroppableContainer } from '@dnd-kit/core' +import type { RectMap } from '@dnd-kit/core/dist/store' +import type { Coordinates } from '@dnd-kit/utilities' + +import { pointerWithin, rectIntersection } from '@dnd-kit/core' + +export const collisionDetectionAlgorithm = (args: { + active: Active + collisionRect: ClientRect + droppableRects: RectMap + droppableContainers: DroppableContainer[] + pointerCoordinates: Coordinates | null +}) => { + const type = args.active?.data.current?.type + + if (type === 'card') return pointerWithin(args) + + if (type === 'column') return rectIntersection(args) + + return pointerWithin(args) +} diff --git a/src/features/kanban/dnd/utils/findIndexById.ts b/src/features/kanban/dnd/utils/findIndexById.ts new file mode 100644 index 00000000..528c48d0 --- /dev/null +++ b/src/features/kanban/dnd/utils/findIndexById.ts @@ -0,0 +1,4 @@ +export const findIndexById = ( + array: T[], + id: string +) => array.findIndex(item => item.id === id) diff --git a/src/features/kanban/dnd/utils/index.ts b/src/features/kanban/dnd/utils/index.ts new file mode 100644 index 00000000..c066aca9 --- /dev/null +++ b/src/features/kanban/dnd/utils/index.ts @@ -0,0 +1,2 @@ +export * from './collisionDetectionAlgorithm' +export * from './findIndexById' diff --git a/src/components/dashboard/board/filters/DeadlineFilter.tsx b/src/features/kanban/filters/components/DeadlineFilter.tsx similarity index 79% rename from src/components/dashboard/board/filters/DeadlineFilter.tsx rename to src/features/kanban/filters/components/DeadlineFilter.tsx index c90f6d63..08d45b2f 100644 --- a/src/components/dashboard/board/filters/DeadlineFilter.tsx +++ b/src/features/kanban/filters/components/DeadlineFilter.tsx @@ -1,10 +1,8 @@ import { Root } from '@radix-ui/react-radio-group' -import { RadioInput } from 'components/ui' - -import { useCardFilters } from 'hooks/card' - -import { deadlines } from 'constants/deadlines' +import { useCardFilters } from 'features/kanban/card/hooks' +import { RadioInput } from 'features/kanban/shared/components' +import { DEADLINES } from 'features/kanban/shared/constants' export const DeadlineFilter = () => { const { setSearchParams, cardDeadline } = useCardFilters() @@ -22,7 +20,7 @@ export const DeadlineFilter = () => { className='flex flex-col gap-2' value={cardDeadline} onValueChange={handleParamsChange}> - {deadlines.map(deadline => ( + {DEADLINES.map(deadline => (