diff --git a/frontend/package.json b/frontend/package.json index c7ec783..9bca7a5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,9 +13,11 @@ "@hookform/resolvers": "^3.3.0", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-scroll-area": "^1.0.4", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.1.4", "@radix-ui/react-tooltip": "^1.0.6", + "@tabler/icons-react": "^2.32.0", "@tanstack/react-query": "^4.33.0", "@types/node": "20.4.8", "@types/react": "18.2.18", diff --git a/frontend/src/app/(dashboard)/shell.tsx b/frontend/src/app/(dashboard)/shell.tsx index e661e99..84eb57e 100644 --- a/frontend/src/app/(dashboard)/shell.tsx +++ b/frontend/src/app/(dashboard)/shell.tsx @@ -1,18 +1,23 @@ -"use client" +'use client'; -export default function Shell({ children }: { - children: React.ReactNode -}) { +import Sidebar from '@/components/sidebar/sidebar'; +import TopBar from '@/components/sidebar/topbar'; +export default function Shell({ children }: { children: React.ReactNode }) { return (
+
-
+ + +
{children}
-
); } diff --git a/frontend/src/components/sidebar/actions/sidebar-actions.tsx b/frontend/src/components/sidebar/actions/sidebar-actions.tsx new file mode 100644 index 0000000..8bffc71 --- /dev/null +++ b/frontend/src/components/sidebar/actions/sidebar-actions.tsx @@ -0,0 +1,37 @@ +import { ReactNode } from 'react'; +import { + IconHome, + IconSearch, + IconSettings, + IconFilePlus, +} from '@tabler/icons-react'; + +export type NavigationMenuType = { + label: string; + path: string; + icon: ReactNode; + isActive?: boolean; + onClick?: React.MouseEventHandler; +}; +export const navigationMenu: NavigationMenuType[] = [ + { + label: 'Home', + path: '', + icon: , + }, + { + label: 'Search', + path: '', + icon: , + }, + { + label: 'Settings', + path: '', + icon: , + }, + { + label: 'New Page', + path: '', + icon: , + }, +]; diff --git a/frontend/src/components/sidebar/atoms/sidebar-atom.ts b/frontend/src/components/sidebar/atoms/sidebar-atom.ts new file mode 100644 index 0000000..a5d6f5d --- /dev/null +++ b/frontend/src/components/sidebar/atoms/sidebar-atom.ts @@ -0,0 +1,5 @@ +import { atomWithWebStorage } from "@/lib/jotai-helper"; +import { atom } from "jotai"; + +export const desktopSidebarAtom = atomWithWebStorage('showSidebar',true); +export const mobileSidebarAtom = atom(false); diff --git a/frontend/src/components/sidebar/hooks/use-toggle-sidebar.ts b/frontend/src/components/sidebar/hooks/use-toggle-sidebar.ts new file mode 100644 index 0000000..5dbe3ba --- /dev/null +++ b/frontend/src/components/sidebar/hooks/use-toggle-sidebar.ts @@ -0,0 +1,8 @@ +import { useAtom } from "jotai"; + +export function useToggleSidebar(sidebarAtom) { + const [sidebarState, setSidebarState] = useAtom(sidebarAtom); + return () => { + setSidebarState(!sidebarState); + } +} diff --git a/frontend/src/components/sidebar/sidebar-section.tsx b/frontend/src/components/sidebar/sidebar-section.tsx new file mode 100644 index 0000000..3c1cb4d --- /dev/null +++ b/frontend/src/components/sidebar/sidebar-section.tsx @@ -0,0 +1,15 @@ +import React, { ReactNode } from "react"; +import { cn } from "@/lib/utils"; + +interface SidebarSectionProps { + className?: string; + children: ReactNode +} + +export function SidebarSection({className, children}: SidebarSectionProps) { + return ( +
+ {children} +
+ ) +} diff --git a/frontend/src/components/sidebar/sidebar.tsx b/frontend/src/components/sidebar/sidebar.tsx new file mode 100644 index 0000000..d67713a --- /dev/null +++ b/frontend/src/components/sidebar/sidebar.tsx @@ -0,0 +1,62 @@ +import { useIsMobile } from '@/hooks/use-is-mobile'; +import { useAtom } from 'jotai'; +import { + desktopSidebarAtom, + mobileSidebarAtom, +} from '@/components/sidebar/atoms/sidebar-atom'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { IconFileText } from '@tabler/icons-react'; +import { SidebarSection } from '@/components/sidebar/sidebar-section'; +import { + navigationMenu, + NavigationMenuType, +} from '@/components/sidebar/actions/sidebar-actions'; +import ButtonWithIcon from '@/components/ui/button-with-icon'; + +export default function Sidebar() { + const isMobile = useIsMobile(); + const [isSidebarOpen] = useAtom( + isMobile ? mobileSidebarAtom : desktopSidebarAtom + ); + + return ( + + ); +} diff --git a/frontend/src/components/sidebar/topbar.tsx b/frontend/src/components/sidebar/topbar.tsx new file mode 100644 index 0000000..a76ad75 --- /dev/null +++ b/frontend/src/components/sidebar/topbar.tsx @@ -0,0 +1,38 @@ +'use client'; + +import { ReactNode } from 'react'; + +import { useIsMobile } from '@/hooks/use-is-mobile'; +import { + desktopSidebarAtom, + mobileSidebarAtom, +} from '@/components/sidebar/atoms/sidebar-atom'; +import { useToggleSidebar } from './hooks/use-toggle-sidebar'; +import ButtonWithIcon from '../ui/button-with-icon'; +import { IconLayoutSidebarLeftCollapse } from '@tabler/icons-react'; + +export default function TopBar() { + const isMobile = useIsMobile(); + const sidebarStateAtom = isMobile ? mobileSidebarAtom : desktopSidebarAtom; + + const toggleSidebar = useToggleSidebar(sidebarStateAtom); + + return ( +
+
+
+
+ } + variant={'ghost'} + onClick={toggleSidebar} + > +
+
+
+
+ ); +} diff --git a/frontend/src/components/ui/button-with-icon.tsx b/frontend/src/components/ui/button-with-icon.tsx new file mode 100644 index 0000000..2069f21 --- /dev/null +++ b/frontend/src/components/ui/button-with-icon.tsx @@ -0,0 +1,18 @@ +import { ReactNode } from 'react'; +import { Button } from '@/components/ui/button'; + +interface ButtonIconProps { + icon: ReactNode; + children?: ReactNode; +} + +type Props = ButtonIconProps & React.ComponentPropsWithoutRef; + +export default function ButtonWithIcon({ icon, children, ...rest }: Props) { + return ( + + ); +} diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index 4ecf369..b45cd50 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -24,7 +24,7 @@ const buttonVariants = cva( default: "h-9 px-4 py-2", sm: "h-8 rounded-md px-3 text-xs", lg: "h-10 rounded-md px-8", - icon: "h-9 w-9", + icon: "h-7 w-7", }, }, defaultVariants: { diff --git a/frontend/src/components/ui/scroll-area.tsx b/frontend/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..54b87cd --- /dev/null +++ b/frontend/src/components/ui/scroll-area.tsx @@ -0,0 +1,48 @@ +"use client" + +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/lib/utils" + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } diff --git a/frontend/src/features/auth/hooks/use-auth.ts b/frontend/src/features/auth/hooks/use-auth.ts index f092fc5..d192825 100644 --- a/frontend/src/features/auth/hooks/use-auth.ts +++ b/frontend/src/features/auth/hooks/use-auth.ts @@ -6,7 +6,6 @@ import { useAtom } from "jotai"; import { authTokensAtom } from "@/features/auth/atoms/auth-tokens-atom"; import { currentUserAtom } from "@/features/user/atoms/current-user-atom"; import { ILogin, IRegister } from "@/features/auth/types/auth.types"; -import { RESET } from "jotai/vanilla/utils/constants"; export default function useAuth() { const [isLoading, setIsLoading] = useState(false); @@ -21,7 +20,7 @@ export default function useAuth() { try { const res = await login(data); setIsLoading(false); - await setAuthToken(res.tokens); + setAuthToken(res.tokens); router.push("/home"); } catch (err) { @@ -40,7 +39,7 @@ export default function useAuth() { const res = await register(data); setIsLoading(false); - await setAuthToken(res.tokens); + setAuthToken(res.tokens); router.push("/home"); } catch (err) { @@ -57,7 +56,7 @@ export default function useAuth() { }; const handleLogout = async () => { - await setAuthToken(RESET); + setAuthToken(null); setCurrentUser(''); } diff --git a/frontend/src/hooks/use-is-mobile.ts b/frontend/src/hooks/use-is-mobile.ts new file mode 100644 index 0000000..d07f36d --- /dev/null +++ b/frontend/src/hooks/use-is-mobile.ts @@ -0,0 +1,5 @@ +import { useMediaQuery } from '@/hooks/use-media-query'; + +export function useIsMobile(): boolean { + return useMediaQuery(`(max-width: 768px)`); +} diff --git a/frontend/src/hooks/use-media-query.ts b/frontend/src/hooks/use-media-query.ts new file mode 100644 index 0000000..316c5d4 --- /dev/null +++ b/frontend/src/hooks/use-media-query.ts @@ -0,0 +1,22 @@ +import { useEffect, useState } from 'react'; + +export function useMediaQuery(query: string): boolean { + const [matches, setMatches] = useState(false); + + useEffect(() => { + const media = window.matchMedia(query); + if (media.matches !== matches) { + setMatches(media.matches); + } + + const listener = () => { + setMatches(media.matches); + }; + + media.addEventListener('change', listener); + + return () => media.removeEventListener('change', listener); + }, [matches, query]); + + return matches; +}