From 636967977f0b9df1da687ea539dc859da69b50c3 Mon Sep 17 00:00:00 2001 From: K Date: Mon, 25 Nov 2024 21:20:47 +0000 Subject: [PATCH] feat: sidebar --- app/globals.css | 16 + app/layout.tsx | 73 +- app/page.tsx | 16 - components/app-sidebar.tsx | 66 ++ components/env-var-warning.tsx | 33 - components/form-message.tsx | 24 - components/hero.tsx | 44 - components/next-logo.tsx | 46 -- components/submit-button.tsx | 23 - components/supabase-logo.tsx | 102 --- components/tutorial/code-block.tsx | 61 -- .../tutorial/connect-supabase-steps.tsx | 62 -- components/tutorial/fetch-data-steps.tsx | 96 --- components/tutorial/sign-up-user-steps.tsx | 88 -- components/tutorial/tutorial-step.tsx | 30 - components/ui/badge.tsx | 36 - components/ui/button.tsx | 2 +- components/ui/checkbox.tsx | 30 - components/ui/dropdown-menu.tsx | 200 ----- components/ui/input.tsx | 19 +- components/ui/label.tsx | 26 - components/ui/separator.tsx | 31 + components/ui/sheet.tsx | 140 ++++ components/ui/sidebar.tsx | 763 ++++++++++++++++++ components/ui/skeleton.tsx | 15 + components/ui/tooltip.tsx | 30 + hooks/use-mobile.tsx | 19 + package-lock.json | 119 +++ package.json | 3 + tailwind.config.ts | 144 ++-- tsconfig.json | 1 + 31 files changed, 1303 insertions(+), 1055 deletions(-) delete mode 100644 app/page.tsx create mode 100644 components/app-sidebar.tsx delete mode 100644 components/env-var-warning.tsx delete mode 100644 components/form-message.tsx delete mode 100644 components/hero.tsx delete mode 100644 components/next-logo.tsx delete mode 100644 components/submit-button.tsx delete mode 100644 components/supabase-logo.tsx delete mode 100644 components/tutorial/code-block.tsx delete mode 100644 components/tutorial/connect-supabase-steps.tsx delete mode 100644 components/tutorial/fetch-data-steps.tsx delete mode 100644 components/tutorial/sign-up-user-steps.tsx delete mode 100644 components/tutorial/tutorial-step.tsx delete mode 100644 components/ui/badge.tsx delete mode 100644 components/ui/checkbox.tsx delete mode 100644 components/ui/dropdown-menu.tsx delete mode 100644 components/ui/label.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/sidebar.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 hooks/use-mobile.tsx diff --git a/app/globals.css b/app/globals.css index f450d1e..052e334 100644 --- a/app/globals.css +++ b/app/globals.css @@ -29,6 +29,14 @@ --chart-3: 197 37% 24%; --chart-4: 43 74% 66%; --chart-5: 27 87% 67%; + --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 { @@ -56,6 +64,14 @@ --chart-3: 30 80% 55%; --chart-4: 280 65% 60%; --chart-5: 340 75% 55%; + --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/app/layout.tsx b/app/layout.tsx index 8233d41..926216b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,67 +1,14 @@ -import { EnvVarWarning } from '@/components/env-var-warning'; -import HeaderAuth from '@/components/header-auth'; -import { hasEnvVars } from '@/utils/supabase/check-env-vars'; -import { GeistSans } from 'geist/font/sans'; -import { ThemeProvider } from 'next-themes'; -import Link from 'next/link'; -import './globals.css'; +import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'; +import { AppSidebar } from '@/components/app-sidebar'; -const defaultUrl = process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}` - : 'http://localhost:3000'; - -export const metadata = { - metadataBase: new URL(defaultUrl), - title: 'Next.js and Supabase Starter Kit', - description: 'The fastest way to build apps with Next.js and Supabase' -}; - -export default function RootLayout({ - children -}: { - children: React.ReactNode; -}) { +export default function Layout({ children }: { children: React.ReactNode }) { return ( - - - -
-
- -
- {children} -
- - -
-
-
- - + + +
+ + {children} +
+
); } diff --git a/app/page.tsx b/app/page.tsx deleted file mode 100644 index ae50e16..0000000 --- a/app/page.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import Hero from '@/components/hero'; -import ConnectSupabaseSteps from '@/components/tutorial/connect-supabase-steps'; -import SignUpUserSteps from '@/components/tutorial/sign-up-user-steps'; -import { hasEnvVars } from '@/utils/supabase/check-env-vars'; - -export default async function Index() { - return ( - <> - -
-

Next steps

- {hasEnvVars ? : } -
- - ); -} diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx new file mode 100644 index 0000000..74aefa4 --- /dev/null +++ b/components/app-sidebar.tsx @@ -0,0 +1,66 @@ +import { Calendar, Home, Inbox, Search, Settings } from 'lucide-react'; +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem +} from '@/components/ui/sidebar'; + +// Menu items. +const items = [ + { + title: 'Home', + url: '#', + icon: Home + }, + { + title: 'Inbox', + url: '#', + icon: Inbox + }, + { + title: 'Calendar', + url: '#', + icon: Calendar + }, + { + title: 'Search', + url: '#', + icon: Search + }, + { + title: 'Settings', + url: '#', + icon: Settings + } +]; + +export function AppSidebar() { + return ( + + + + Application + + + {items.map((item) => ( + + + + + {item.title} + + + + ))} + + + + + + ); +} diff --git a/components/env-var-warning.tsx b/components/env-var-warning.tsx deleted file mode 100644 index 4af21a1..0000000 --- a/components/env-var-warning.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import Link from 'next/link'; -import { Badge } from './ui/badge'; -import { Button } from './ui/button'; - -export function EnvVarWarning() { - return ( -
- - Supabase environment variables required - -
- - -
-
- ); -} diff --git a/components/form-message.tsx b/components/form-message.tsx deleted file mode 100644 index eebbdc3..0000000 --- a/components/form-message.tsx +++ /dev/null @@ -1,24 +0,0 @@ -export type Message = - | { success: string } - | { error: string } - | { message: string }; - -export function FormMessage({ message }: { message: Message }) { - return ( -
- {'success' in message && ( -
- {message.success} -
- )} - {'error' in message && ( -
- {message.error} -
- )} - {'message' in message && ( -
{message.message}
- )} -
- ); -} diff --git a/components/hero.tsx b/components/hero.tsx deleted file mode 100644 index 971c146..0000000 --- a/components/hero.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import NextLogo from './next-logo'; -import SupabaseLogo from './supabase-logo'; - -export default function Header() { - return ( -
-
- - - - - - - -
-

Supabase and Next.js Starter Template

-

- The fastest way to build apps with{' '} - - Supabase - {' '} - and{' '} - - Next.js - -

-
-
- ); -} diff --git a/components/next-logo.tsx b/components/next-logo.tsx deleted file mode 100644 index beb120a..0000000 --- a/components/next-logo.tsx +++ /dev/null @@ -1,46 +0,0 @@ -export default function NextLogo() { - return ( - - - - - - - - - - - ); -} diff --git a/components/submit-button.tsx b/components/submit-button.tsx deleted file mode 100644 index 0d31f55..0000000 --- a/components/submit-button.tsx +++ /dev/null @@ -1,23 +0,0 @@ -'use client'; - -import { Button } from '@/components/ui/button'; -import { type ComponentProps } from 'react'; -import { useFormStatus } from 'react-dom'; - -type Props = ComponentProps & { - pendingText?: string; -}; - -export function SubmitButton({ - children, - pendingText = 'Submitting...', - ...props -}: Props) { - const { pending } = useFormStatus(); - - return ( - - ); -} diff --git a/components/supabase-logo.tsx b/components/supabase-logo.tsx deleted file mode 100644 index d5ea111..0000000 --- a/components/supabase-logo.tsx +++ /dev/null @@ -1,102 +0,0 @@ -export default function SupabaseLogo() { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/components/tutorial/code-block.tsx b/components/tutorial/code-block.tsx deleted file mode 100644 index 867e949..0000000 --- a/components/tutorial/code-block.tsx +++ /dev/null @@ -1,61 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { Button } from '../ui/button'; - -const CopyIcon = () => ( - - - - -); - -const CheckIcon = () => ( - - - -); - -export function CodeBlock({ code }: { code: string }) { - const [icon, setIcon] = useState(CopyIcon); - - const copy = async () => { - await navigator?.clipboard?.writeText(code); - setIcon(CheckIcon); - setTimeout(() => setIcon(CopyIcon), 2000); - }; - - return ( -
-      
-      {code}
-    
- ); -} diff --git a/components/tutorial/connect-supabase-steps.tsx b/components/tutorial/connect-supabase-steps.tsx deleted file mode 100644 index 25c0d29..0000000 --- a/components/tutorial/connect-supabase-steps.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { TutorialStep } from './tutorial-step'; - -export default function ConnectSupabaseSteps() { - return ( -
    - -

    - Head over to{' '} - - database.new - {' '} - and create a new Supabase project. -

    -
    - - -

    - Rename the{' '} - - .env.example - {' '} - file in your Next.js app to{' '} - - .env.local - {' '} - and populate with values from{' '} - - your Supabase project's API Settings - - . -

    -
    - - -

    - You may need to quit your Next.js development server and run{' '} - - npm run dev - {' '} - again to load the new environment variables. -

    -
    - - -

    - You may need to refresh the page for Next.js to load the new - environment variables. -

    -
    -
- ); -} diff --git a/components/tutorial/fetch-data-steps.tsx b/components/tutorial/fetch-data-steps.tsx deleted file mode 100644 index 5447487..0000000 --- a/components/tutorial/fetch-data-steps.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { TutorialStep } from './tutorial-step'; -import { CodeBlock } from './code-block'; - -const create = `create table notes ( - id bigserial primary key, - title text -); - -insert into notes(title) -values - ('Today I created a Supabase project.'), - ('I added some data and queried it from Next.js.'), - ('It was awesome!'); -`.trim(); - -const server = `import { createClient } from '@/utils/supabase/server' - -export default async function Page() { - const supabase = createClient() - const { data: notes } = await supabase.from('notes').select() - - return
{JSON.stringify(notes, null, 2)}
-} -`.trim(); - -const client = `'use client' - -import { createClient } from '@/utils/supabase/client' -import { useEffect, useState } from 'react' - -export default function Page() { - const [notes, setNotes] = useState(null) - const supabase = createClient() - - useEffect(() => { - const getData = async () => { - const { data } = await supabase.from('notes').select() - setNotes(data) - } - getData() - }, []) - - return
{JSON.stringify(notes, null, 2)}
-} -`.trim(); - -export default function FetchDataSteps() { - return ( -
    - -

    - Head over to the{' '} - - Table Editor - {' '} - for your Supabase project to create a table and insert some example - data. If you're stuck for creativity, you can copy and paste the - following into the{' '} - - SQL Editor - {' '} - and click RUN! -

    - -
    - - -

    - To create a Supabase client and query data from an Async Server - Component, create a new page.tsx file at{' '} - - /app/notes/page.tsx - {' '} - and add the following. -

    - -

    Alternatively, you can use a Client Component.

    - -
    - - -

    You're ready to launch your product to the world! 🚀

    -
    -
- ); -} diff --git a/components/tutorial/sign-up-user-steps.tsx b/components/tutorial/sign-up-user-steps.tsx deleted file mode 100644 index 3036717..0000000 --- a/components/tutorial/sign-up-user-steps.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import Link from 'next/link'; -import { TutorialStep } from './tutorial-step'; -import { ArrowUpRight } from 'lucide-react'; - -export default function SignUpUserSteps() { - return ( -
    - {process.env.VERCEL_ENV === 'preview' || - process.env.VERCEL_ENV === 'production' ? ( - -

    It looks like this App is hosted on Vercel.

    -

    - This particular deployment is - - "{process.env.VERCEL_ENV}" - {' '} - on - - https://{process.env.VERCEL_URL} - - . -

    -

    - You will need to{' '} - - update your Supabase project - {' '} - with redirect URLs based on your Vercel deployment URLs. -

    -
      -
    • - -{' '} - - http://localhost:3000/** - -
    • -
    • - -{' '} - - {`https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}/**`} - -
    • -
    • - -{' '} - - {`https://${process.env.VERCEL_PROJECT_PRODUCTION_URL?.replace('.vercel.app', '')}-*-[vercel-team-url].vercel.app/**`} - {' '} - (Vercel Team URL can be found in{' '} - - Vercel Team settings - - ) -
    • -
    - - Redirect URLs Docs - -
    - ) : null} - -

    - Head over to the{' '} - - Sign up - {' '} - page and sign up your first user. It's okay if this is just you for - now. Your awesome idea will have plenty of users later! -

    -
    -
- ); -} diff --git a/components/tutorial/tutorial-step.tsx b/components/tutorial/tutorial-step.tsx deleted file mode 100644 index 5bd957e..0000000 --- a/components/tutorial/tutorial-step.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Checkbox } from '../ui/checkbox'; - -export function TutorialStep({ - title, - children -}: { - title: string; - children: React.ReactNode; -}) { - return ( -
  • - - -
  • - ); -} diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx deleted file mode 100644 index 6c3e159..0000000 --- a/components/ui/badge.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import * as React from 'react'; -import { cva, type VariantProps } from 'class-variance-authority'; - -import { cn } from '@/lib/utils'; - -const badgeVariants = cva( - 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', - { - variants: { - variant: { - default: - 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', - secondary: - 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', - destructive: - 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', - outline: 'text-foreground' - } - }, - defaultVariants: { - variant: 'default' - } - } -); - -export interface BadgeProps - extends React.HTMLAttributes, - VariantProps {} - -function Badge({ className, variant, ...props }: BadgeProps) { - return ( -
    - ); -} - -export { Badge, badgeVariants }; diff --git a/components/ui/button.tsx b/components/ui/button.tsx index 473f585..c7b0fde 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -5,7 +5,7 @@ import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils'; const buttonVariants = cva( - 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', + 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', { variants: { variant: { diff --git a/components/ui/checkbox.tsx b/components/ui/checkbox.tsx deleted file mode 100644 index 63f4ef3..0000000 --- a/components/ui/checkbox.tsx +++ /dev/null @@ -1,30 +0,0 @@ -'use client'; - -import * as React from 'react'; -import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; -import { Check } from 'lucide-react'; - -import { cn } from '@/lib/utils'; - -const Checkbox = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - - - -)); -Checkbox.displayName = CheckboxPrimitive.Root.displayName; - -export { Checkbox }; diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx deleted file mode 100644 index 4ca6700..0000000 --- a/components/ui/dropdown-menu.tsx +++ /dev/null @@ -1,200 +0,0 @@ -'use client'; - -import * as React from 'react'; -import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; -import { Check, ChevronRight, Circle } from 'lucide-react'; - -import { cn } from '@/lib/utils'; - -const DropdownMenu = DropdownMenuPrimitive.Root; - -const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; - -const DropdownMenuGroup = DropdownMenuPrimitive.Group; - -const DropdownMenuPortal = DropdownMenuPrimitive.Portal; - -const DropdownMenuSub = DropdownMenuPrimitive.Sub; - -const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; - -const DropdownMenuSubTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean; - } ->(({ className, inset, children, ...props }, ref) => ( - - {children} - - -)); -DropdownMenuSubTrigger.displayName = - DropdownMenuPrimitive.SubTrigger.displayName; - -const DropdownMenuSubContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -DropdownMenuSubContent.displayName = - DropdownMenuPrimitive.SubContent.displayName; - -const DropdownMenuContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, sideOffset = 4, ...props }, ref) => ( - - - -)); -DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; - -const DropdownMenuItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean; - } ->(({ className, inset, ...props }, ref) => ( - -)); -DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; - -const DropdownMenuCheckboxItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, checked, ...props }, ref) => ( - - - - - - - {children} - -)); -DropdownMenuCheckboxItem.displayName = - DropdownMenuPrimitive.CheckboxItem.displayName; - -const DropdownMenuRadioItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - - - - - {children} - -)); -DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; - -const DropdownMenuLabel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean; - } ->(({ className, inset, ...props }, ref) => ( - -)); -DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; - -const DropdownMenuSeparator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; - -const DropdownMenuShortcut = ({ - className, - ...props -}: React.HTMLAttributes) => { - return ( - - ); -}; -DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'; - -export { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuCheckboxItem, - DropdownMenuRadioItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuGroup, - DropdownMenuPortal, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuRadioGroup -}; diff --git a/components/ui/input.tsx b/components/ui/input.tsx index d2008f0..68551b9 100644 --- a/components/ui/input.tsx +++ b/components/ui/input.tsx @@ -1,25 +1,22 @@ -import * as React from 'react'; +import * as React from "react" -import { cn } from '@/lib/utils'; +import { cn } from "@/lib/utils" -export interface InputProps - extends React.InputHTMLAttributes {} - -const Input = React.forwardRef( +const Input = React.forwardRef>( ({ className, type, ...props }, ref) => { return ( - ); + ) } -); -Input.displayName = 'Input'; +) +Input.displayName = "Input" -export { Input }; +export { Input } diff --git a/components/ui/label.tsx b/components/ui/label.tsx deleted file mode 100644 index 1e24ec0..0000000 --- a/components/ui/label.tsx +++ /dev/null @@ -1,26 +0,0 @@ -'use client'; - -import * as React from 'react'; -import * as LabelPrimitive from '@radix-ui/react-label'; -import { cva, type VariantProps } from 'class-variance-authority'; - -import { cn } from '@/lib/utils'; - -const labelVariants = cva( - 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70' -); - -const Label = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & - VariantProps ->(({ className, ...props }, ref) => ( - -)); -Label.displayName = LabelPrimitive.Root.displayName; - -export { Label }; diff --git a/components/ui/separator.tsx b/components/ui/separator.tsx new file mode 100644 index 0000000..12d81c4 --- /dev/null +++ b/components/ui/separator.tsx @@ -0,0 +1,31 @@ +"use client" + +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "@/lib/utils" + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( + + ) +) +Separator.displayName = SeparatorPrimitive.Root.displayName + +export { Separator } diff --git a/components/ui/sheet.tsx b/components/ui/sheet.tsx new file mode 100644 index 0000000..a37f17b --- /dev/null +++ b/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/components/ui/sidebar.tsx b/components/ui/sidebar.tsx new file mode 100644 index 0000000..12f01c0 --- /dev/null +++ b/components/ui/sidebar.tsx @@ -0,0 +1,763 @@ +'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/use-mobile'; +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 ( +