From 2ac45b6fca81f99a6308aa5401dce823720ef3c8 Mon Sep 17 00:00:00 2001 From: Smnthjm08 Date: Fri, 15 Nov 2024 23:56:39 +0530 Subject: [PATCH] sidebar init and basic layout added --- package.json | 1 + src/app/(new)/new/layout.tsx | 15 + src/app/(new)/new/page.tsx | 6 + src/app/globals.css | 26 +- src/app/layout.tsx | 4 +- src/components/new/app-sidebar.tsx | 229 +++++++++ src/components/new/nav-main.tsx | 74 +++ src/components/new/nav-projects.tsx | 89 ++++ src/components/new/nav-user.tsx | 147 ++++++ src/components/ui/collapsible.tsx | 11 + src/components/ui/sheet.tsx | 140 +++++ src/components/ui/sidebar.tsx | 771 ++++++++++++++++++++++++++++ src/hooks/useMobile.tsx | 21 + tailwind.config.js | 36 +- 14 files changed, 1560 insertions(+), 10 deletions(-) create mode 100644 src/app/(new)/new/layout.tsx create mode 100644 src/app/(new)/new/page.tsx create mode 100644 src/components/new/app-sidebar.tsx create mode 100644 src/components/new/nav-main.tsx create mode 100644 src/components/new/nav-projects.tsx create mode 100644 src/components/new/nav-user.tsx create mode 100644 src/components/ui/collapsible.tsx create mode 100644 src/components/ui/sheet.tsx create mode 100644 src/components/ui/sidebar.tsx create mode 100644 src/hooks/useMobile.tsx diff --git a/package.json b/package.json index 466ac97a5..15fa524fc 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@prisma/client": "^5.18.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-collapsible": "^1.1.1", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", diff --git a/src/app/(new)/new/layout.tsx b/src/app/(new)/new/layout.tsx new file mode 100644 index 000000000..b1fd878c1 --- /dev/null +++ b/src/app/(new)/new/layout.tsx @@ -0,0 +1,15 @@ +import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'; +import { AppSidebar } from '@/components/new/app-sidebar'; +import React from 'react'; + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + + +
+ + {children} +
+
+ ); +} diff --git a/src/app/(new)/new/page.tsx b/src/app/(new)/new/page.tsx new file mode 100644 index 000000000..e10053ba3 --- /dev/null +++ b/src/app/(new)/new/page.tsx @@ -0,0 +1,6 @@ +export default function Page() { + return ( +
+
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index 7db494853..16b81d103 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -355,4 +355,28 @@ to { mask-size: 350vmax; } -} \ No newline at end of file +} + +@layer base { + :root { + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; + } + + .dark { + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f5198fcba..1c02b4093 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -7,7 +7,7 @@ import { Providers } from './providers'; import { GoogleAnalytics } from '@/components/analytics/GoogleAnalytics'; import { siteConfig } from '@/config/site-config'; import { Toaster } from 'sonner'; -import { Navbar } from '@/components/Navbar'; +// import { Navbar } from '@/components/Navbar'; import NextTopLoader from 'nextjs-toploader'; import OfflineNotification from '@/components/OfflineNavigator'; @@ -38,7 +38,7 @@ export default function RootLayout({ children }: { children: ReactNode }) { showSpinner={false} /> - + {/* */} {children} diff --git a/src/components/new/app-sidebar.tsx b/src/components/new/app-sidebar.tsx new file mode 100644 index 000000000..628b92d85 --- /dev/null +++ b/src/components/new/app-sidebar.tsx @@ -0,0 +1,229 @@ +'use client'; + +import * as React from 'react'; + +import { + Sidebar, + SidebarContent, + SidebarFooter, + // SidebarGroup, + SidebarHeader, + SidebarMenu, + // SidebarMenuButton, + SidebarMenuItem, + // SidebarMenuSub, + // SidebarMenuSubButton, + // SidebarMenuSubItem, + SidebarRail, +} from '@/components/ui/sidebar'; +import Link from 'next/link'; +import { useSession } from 'next-auth/react'; +import { Button } from '../ui/button'; +import { usePathname, useRouter } from 'next/navigation'; +import { + ArrowLeft, + AudioWaveform, + BookOpen, + Bot, + Command, + CommandIcon, + Frame, + GalleryVerticalEnd, + PieChart, + Settings2, +} from 'lucide-react'; +import { NavMain } from './nav-main'; +import { NavUser } from './nav-user'; +// import { NavProjects } from './nav-projects'; + +// This is sample data. +const data = { + user: { + name: 'shadcn', + email: 'm@example.com', + avatar: '/avatars/shadcn.jpg', + }, + teams: [ + { + name: 'Acme Inc', + logo: GalleryVerticalEnd, + plan: 'Enterprise', + }, + { + name: 'Acme Corp.', + logo: AudioWaveform, + plan: 'Startup', + }, + { + name: 'Evil Corp.', + logo: Command, + plan: 'Free', + }, + ], + navMain: [ + { + title: 'Week 0', + url: '#', + icon: CommandIcon, + isActive: true, + items: [ + { + title: 'Intro, Setting Up Your IDE', + url: '#', + }, + { + title: 'HTML Basics', + url: '#', + }, + { + title: 'Css Basics', + url: '#', + }, + ], + }, + { + title: 'Models', + url: '#', + icon: Bot, + items: [ + { + title: 'Genesis', + url: '#', + }, + { + title: 'Explorer', + url: '#', + }, + { + title: 'Quantum', + url: '#', + }, + ], + }, + { + title: 'Documentation', + url: '#', + icon: BookOpen, + items: [ + { + title: 'Introduction', + url: '#', + }, + { + title: 'Get Started', + url: '#', + }, + { + title: 'Tutorials', + url: '#', + }, + { + title: 'Changelog', + url: '#', + }, + ], + }, + { + title: 'Settings', + url: '#', + icon: Settings2, + items: [ + { + title: 'General', + url: '#', + }, + { + title: 'Team', + url: '#', + }, + { + title: 'Billing', + url: '#', + }, + { + title: 'Limits', + url: '#', + }, + ], + }, + ], + projects: [ + { + name: 'Design Engineering', + url: '#', + icon: Frame, + }, + { + name: 'Sales & Marketing', + url: '#', + icon: PieChart, + }, + { + name: 'Travel', + url: '#', + icon: Map, + }, + ], +}; + +export function AppSidebar({ ...props }: React.ComponentProps) { + const { data: session } = useSession(); + const router = useRouter(); + const pathname = usePathname(); + + return ( + + + + + {/* */} + + {/* */} + + + + + + {/* */} + + + + + + + ); +} diff --git a/src/components/new/nav-main.tsx b/src/components/new/nav-main.tsx new file mode 100644 index 000000000..af34f41da --- /dev/null +++ b/src/components/new/nav-main.tsx @@ -0,0 +1,74 @@ +'use client'; + +import { ChevronRight, type LucideIcon } from 'lucide-react'; + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger + } from "@/components/ui/collapsible"; + +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, +} from '@/components/ui/sidebar'; + +export function NavMain({ + items, +}: { + items: { + title: string; + url: string; + icon?: LucideIcon; + isActive?: boolean; + items?: { + title: string; + url: string; + }[]; + }[]; +}) { + return ( + + Platform + + {items.map((item) => ( + + + + + {item.icon && } + {item.title} + + + + + + {item.items?.map((subItem) => ( + + + + {subItem.title} + + + + ))} + + + + + ))} + + + ); +} diff --git a/src/components/new/nav-projects.tsx b/src/components/new/nav-projects.tsx new file mode 100644 index 000000000..7986c0ea6 --- /dev/null +++ b/src/components/new/nav-projects.tsx @@ -0,0 +1,89 @@ +'use client'; + +import { + Folder, + Forward, + MoreHorizontal, + Trash2, + type LucideIcon, +} from 'lucide-react'; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuAction, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from '@/components/ui/sidebar'; + +export function NavProjects({ + projects, +}: { + projects: { + name: string; + url: string; + icon: LucideIcon; + }[]; +}) { + const { isMobile } = useSidebar(); + + return ( + + Projects + + {projects.map((item) => ( + + + + + {item.name} + + + + + + + More + + + + + + View Project + + + + Share Project + + + + + Delete Project + + + + + ))} + + + + More + + + + + ); +} diff --git a/src/components/new/nav-user.tsx b/src/components/new/nav-user.tsx new file mode 100644 index 000000000..4a7fd07d4 --- /dev/null +++ b/src/components/new/nav-user.tsx @@ -0,0 +1,147 @@ +'use client'; + +import { + Bookmark, + Calendar, + ChevronsUpDown, + CreditCard, + History, + LogOut, + MessageSquare, +} from 'lucide-react'; + +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from '@/components/ui/sidebar'; +import ExternalLinks from '../profile-menu/ExternalLinks'; +import Link from 'next/link'; +import { signOut, useSession } from 'next-auth/react'; +import ProfileDropdown from '../profile-menu/ProfileDropdown'; + +export function NavUser({ + user, +}: { + user: { + name: string; + email: string; + avatar: string; + }; +}) { + const { isMobile } = useSidebar(); + const { data: session } = useSession(); + + const menuItemLinks = [ + { + href: '/watch-history', + icon: , + label: 'Watch History', + }, + { + href: '/bookmark', + icon: , + label: 'Bookmarks', + }, + { + href: '/question', + icon: , + label: 'Questions', + }, + { + href: '/payout-methods', + icon: , + label: 'Payout Methods', + }, + { + href: '/calendar', + icon: , + label: 'Calendar', + }, + ]; + + return ( + + + + + + + + CN + +
+ {user.name} + {user.email} +
+ +
+
+ + + +
+ + + CN + +
+ {user.name} + {user.email} +
+
+
+ + + {menuItemLinks.map(({ href, label, icon }) => ( + + + {icon} + {label} + + + ))} + + + + + + { + signOut(); + }} + > + + + Logout + + +
+ {session?.user && } + +
+
+
+ ); +} diff --git a/src/components/ui/collapsible.tsx b/src/components/ui/collapsible.tsx new file mode 100644 index 000000000..9fa48946a --- /dev/null +++ b/src/components/ui/collapsible.tsx @@ -0,0 +1,11 @@ +"use client" + +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" + +const Collapsible = CollapsiblePrimitive.Root + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx new file mode 100644 index 000000000..a37f17ba0 --- /dev/null +++ b/src/components/ui/sheet.tsx @@ -0,0 +1,140 @@ +"use client" + +import * as React from "react" +import * as SheetPrimitive from "@radix-ui/react-dialog" +import { cva, type VariantProps } from "class-variance-authority" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Sheet = SheetPrimitive.Root + +const SheetTrigger = SheetPrimitive.Trigger + +const SheetClose = SheetPrimitive.Close + +const SheetPortal = SheetPrimitive.Portal + +const SheetOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetOverlay.displayName = SheetPrimitive.Overlay.displayName + +const sheetVariants = cva( + "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", + { + variants: { + side: { + top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", + bottom: + "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", + left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", + right: + "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", + }, + }, + defaultVariants: { + side: "right", + }, + } +) + +interface SheetContentProps + extends React.ComponentPropsWithoutRef, + VariantProps {} + +const SheetContent = React.forwardRef< + React.ElementRef, + SheetContentProps +>(({ side = "right", className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +SheetContent.displayName = SheetPrimitive.Content.displayName + +const SheetHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +SheetHeader.displayName = "SheetHeader" + +const SheetFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +SheetFooter.displayName = "SheetFooter" + +const SheetTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetTitle.displayName = SheetPrimitive.Title.displayName + +const SheetDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetDescription.displayName = SheetPrimitive.Description.displayName + +export { + Sheet, + SheetPortal, + SheetOverlay, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx new file mode 100644 index 000000000..eb58669de --- /dev/null +++ b/src/components/ui/sidebar.tsx @@ -0,0 +1,771 @@ +'use client'; + +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { VariantProps, cva } from 'class-variance-authority'; +import { PanelLeft } from 'lucide-react'; + +import { useIsMobile } from '@/hooks/useMobile'; +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Separator } from '@/components/ui/separator'; +import { Sheet, SheetContent } from '@/components/ui/sheet'; +import { Skeleton } from '@/components/ui/skeleton'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; + +const SIDEBAR_COOKIE_NAME = 'sidebar:state'; +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; +const SIDEBAR_WIDTH = '16rem'; +const SIDEBAR_WIDTH_MOBILE = '18rem'; +const SIDEBAR_WIDTH_ICON = '3rem'; +const SIDEBAR_KEYBOARD_SHORTCUT = 'b'; + +type SidebarContext = { + state: 'expanded' | 'collapsed'; + open: boolean; + setOpen: (open: boolean) => void; + openMobile: boolean; + setOpenMobile: (open: boolean) => void; + isMobile: boolean; + toggleSidebar: () => void; +}; + +const SidebarContext = React.createContext(null); + +function useSidebar() { + const context = React.useContext(SidebarContext); + if (!context) { + throw new Error('useSidebar must be used within a SidebarProvider.'); + } + + return context; +} + +const SidebarProvider = React.forwardRef< + HTMLDivElement, + React.ComponentProps<'div'> & { + defaultOpen?: boolean; + open?: boolean; + onOpenChange?:(open: boolean) => void; + } +>( + ( + { + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props + }, + ref, + ) => { + const isMobile = useIsMobile(); + const [openMobile, setOpenMobile] = React.useState(false); + + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = React.useState(defaultOpen); + const open = openProp ?? _open; + const setOpen = React.useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === 'function' ? value(open) : value; + if (setOpenProp) { + setOpenProp(openState); + } else { + _setOpen(openState); + } + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; + }, + [setOpenProp, open], + ); + + // Helper to toggle the sidebar. + const toggleSidebar = React.useCallback(() => { + return isMobile + ? setOpenMobile((open) => !open) + : setOpen((open) => !open); + }, [isMobile, setOpen, setOpenMobile]); + + // Adds a keyboard shortcut to toggle the sidebar. + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.key === SIDEBAR_KEYBOARD_SHORTCUT && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault(); + toggleSidebar(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [toggleSidebar]); + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? 'expanded' : 'collapsed'; + + const contextValue = React.useMemo( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + ], + ); + + return ( + + +
+ {children} +
+
+
+ ); + }, +); +SidebarProvider.displayName = 'SidebarProvider'; + +const Sidebar = React.forwardRef< + HTMLDivElement, + React.ComponentProps<'div'> & { + side?: 'left' | 'right'; + variant?: 'sidebar' | 'floating' | 'inset'; + collapsible?: 'offcanvas' | 'icon' | 'none'; + } +>( + ( + { + side = 'left', + variant = 'sidebar', + collapsible = 'offcanvas', + className, + children, + ...props + }, + ref, + ) => { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); + + if (collapsible === 'none') { + return ( +
+ {children} +
+ ); + } + + if (isMobile) { + return ( + + +
{children}
+
+
+ ); + } + + return ( +
+ {/* This is what handles the sidebar gap on desktop */} +
+ +
+ ); + }, +); +Sidebar.displayName = 'Sidebar'; + +const SidebarTrigger = React.forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, onClick, ...props }, ref) => { + const { toggleSidebar } = useSidebar(); + + return ( + + ); +}); +SidebarTrigger.displayName = 'SidebarTrigger'; + +const SidebarRail = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<'button'> +>(({ className, ...props }, ref) => { + const { toggleSidebar } = useSidebar(); + + return ( +