diff --git a/app/globals.css b/app/globals.css index e72e243..e76b955 100644 --- a/app/globals.css +++ b/app/globals.css @@ -184,3 +184,12 @@ body { linear-gradient(to right, #21c796, #0385ce, #0d40bf) border-box; border: 1px solid transparent; } + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/app/layout.tsx b/app/layout.tsx index 5ee78e7..f17fff4 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,6 +4,8 @@ import type { Metadata } from "next"; import localFont from "next/font/local"; import "./globals.css"; import { ThemeProvider } from "@/components/theme-provider"; +import { SidebarProvider } from "@/components/ui/sidebar"; +import { AppSidebar } from "@/components/app-sidebar"; const Vazir = localFont({ src: [ @@ -40,9 +42,14 @@ export default function RootLayout({ enableSystem disableTransitionOnChange > -
-
{children}
- + + +
+
+
{children}
+ +
+
diff --git a/bun.lockb b/bun.lockb index 17fb56c..c02195a 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx new file mode 100644 index 0000000..7813905 --- /dev/null +++ b/components/app-sidebar.tsx @@ -0,0 +1,71 @@ +import { Calendar, Home, Inbox, Search, Settings } from "lucide-react"; + +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "@/components/ui/sidebar"; +import { ModeToggle } from "./mode-toggle"; +import ToggleLanguage from "./toggle-language"; + +// 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/header.tsx b/components/header.tsx index 04ffe07..3b82081 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -8,10 +8,12 @@ import { useEffect, useState } from "react"; import { ModeToggle } from "./mode-toggle"; import ToggleLanguage from "./toggle-language"; import { Input } from "./ui/input"; +import { useSidebar } from "./ui/sidebar"; export default function Header() { const [isScrollingUp, setIsScrollingUp] = useState(true); // Track scroll direction const [prevScrollPos, setPrevScrollPos] = useState(0); + const { toggleSidebar } = useSidebar(); useEffect(() => { const handleScroll = () => { @@ -36,7 +38,7 @@ export default function Header() { }, [prevScrollPos]); return ( -
+
{/* Left Section */}
@@ -47,12 +49,15 @@ export default function Header() { - - +
+ + +
{/* Right Section */}
- +
diff --git a/components/podcast-hero.tsx b/components/podcast-hero.tsx index 7434762..e82d0e7 100644 --- a/components/podcast-hero.tsx +++ b/components/podcast-hero.tsx @@ -35,7 +35,8 @@ export function PodcastHero() {
-

+

هرگز یک قسمت را از دست ندهید

-

+

در خبرنامه ما مشترک شوید و از انتشار قسمت های جدید مطلع شوید منتشر شد.

@@ -25,9 +25,9 @@ export function SubscribeSection() { type="email" className="sm:min-w-[300px] bg-background border-2 h-10 py-4" /> - diff --git a/components/ui/sheet.tsx b/components/ui/sheet.tsx new file mode 100644 index 0000000..84649ad --- /dev/null +++ b/components/ui/sheet.tsx @@ -0,0 +1,139 @@ +"use client" + +import * as React from "react" +import * as SheetPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Sheet({ ...props }: React.ComponentProps) { + return +} + +function SheetTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function SheetClose({ + ...props +}: React.ComponentProps) { + return +} + +function SheetPortal({ + ...props +}: React.ComponentProps) { + return +} + +function SheetOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetContent({ + className, + children, + side = "right", + ...props +}: React.ComponentProps & { + side?: "top" | "right" | "bottom" | "left" +}) { + return ( + + + + {children} + + + Close + + + + ) +} + +function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Sheet, + 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..7ffbc07 --- /dev/null +++ b/components/ui/sidebar.tsx @@ -0,0 +1,726 @@ +"use client" + +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { VariantProps, cva } from "class-variance-authority" +import { PanelLeftIcon } 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, + SheetDescription, + SheetHeader, + SheetTitle, +} 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 SidebarContextProps = { + 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 +} + +function SidebarProvider({ + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props +}: React.ComponentProps<"div"> & { + defaultOpen?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void +}) { + 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} +
+
+
+ ) +} + +function Sidebar({ + side = "left", + variant = "sidebar", + collapsible = "offcanvas", + className, + children, + ...props +}: React.ComponentProps<"div"> & { + side?: "left" | "right" + variant?: "sidebar" | "floating" | "inset" + collapsible?: "offcanvas" | "icon" | "none" +}) { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar() + + if (collapsible === "none") { + return ( +
+ {children} +
+ ) + } + + if (isMobile) { + return ( + + + + Sidebar + Displays the mobile sidebar. + +
{children}
+
+
+ ) + } + + return ( +
+ {/* This is what handles the sidebar gap on desktop */} +
+ +
+ ) +} + +function SidebarTrigger({ + className, + onClick, + ...props +}: React.ComponentProps) { + const { toggleSidebar } = useSidebar() + + return ( + + ) +} + +function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { + const { toggleSidebar } = useSidebar() + + return ( +