From 87e0577ff730c6672fe4c5f1ca61f280a9968fad Mon Sep 17 00:00:00 2001 From: SomeCodecat <88855796+SomeCodecat@users.noreply.github.com> Date: Mon, 16 Jun 2025 13:01:20 +0200 Subject: [PATCH 1/8] feat: add Radix UI components and implement sidebar functionality - Added new Radix UI components: Dialog, Tooltip, Separator, and updated existing components. - Introduced a Sidebar component with collapsible functionality and mobile responsiveness. - Implemented a custom hook `useIsMobile` to manage mobile state. - Updated package dependencies in package.json and yarn.lock for new components. - Created utility components such as Button, Skeleton, and Input for consistent styling. feat: add AppSidebar component with collapsible functionality and sidebar menu - Introduced AppSidebar component for a customizable sidebar layout. - Implemented collapsible sections using Radix UI's Collapsible component. - Added sidebar menu items with icons and links for navigation. - Created Sidebar UI components including SidebarHeader, SidebarFooter, and SidebarMenu. - Integrated ThemePicker for theme selection within the sidebar. - Updated sidebar styles and layout for better responsiveness. chore: add @radix-ui/react-collapsible dependency - Added @radix-ui/react-collapsible package to manage collapsible UI elements. --- package.json | 9 +- src/app/{ => (main)}/home/page.tsx | 4 +- src/app/(main)/layout.tsx | 23 + src/app/globals.css | 18 +- src/components/custom-ui/app-sidebar.tsx | 147 ++++ src/components/misc/header.tsx | 23 + src/components/misc/logo.tsx | 1 + src/components/ui/collapsible.tsx | 33 + src/components/ui/separator.tsx | 7 +- src/components/ui/sheet.tsx | 139 ++++ src/components/ui/sidebar.tsx | 725 +++++++++++++++++++ src/components/ui/skeleton.tsx | 13 + src/components/ui/tooltip.tsx | 61 ++ src/components/wrappers/sidebar-provider.tsx | 23 + src/hooks/use-mobile.ts | 19 + yarn.lock | 167 ++++- 16 files changed, 1358 insertions(+), 54 deletions(-) rename src/app/{ => (main)}/home/page.tsx (81%) create mode 100644 src/app/(main)/layout.tsx create mode 100644 src/components/custom-ui/app-sidebar.tsx create mode 100644 src/components/misc/header.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/components/ui/skeleton.tsx create mode 100644 src/components/ui/tooltip.tsx create mode 100644 src/components/wrappers/sidebar-provider.tsx create mode 100644 src/hooks/use-mobile.ts diff --git a/package.json b/package.json index c5c77fb..c1273c1 100644 --- a/package.json +++ b/package.json @@ -27,20 +27,23 @@ "@fortawesome/react-fontawesome": "^0.2.2", "@hookform/resolvers": "^5.0.1", "@prisma/client": "^6.9.0", + "@radix-ui/react-collapsible": "^1.1.11", + "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.14", "@radix-ui/react-hover-card": "^1.1.13", "@radix-ui/react-label": "^2.1.6", "@radix-ui/react-scroll-area": "^1.2.8", "@radix-ui/react-select": "^2.2.4", - "@radix-ui/react-separator": "^1.1.6", - "@radix-ui/react-slot": "^1.2.2", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.4", "@radix-ui/react-tabs": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.7", "@tanstack/react-query": "^5.80.7", "bcryptjs": "^3.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "lucide-react": "^0.511.0", + "lucide-react": "^0.515.0", "next": "15.4.0-canary.92", "next-auth": "^5.0.0-beta.25", "next-themes": "^0.4.6", diff --git a/src/app/home/page.tsx b/src/app/(main)/home/page.tsx similarity index 81% rename from src/app/home/page.tsx rename to src/app/(main)/home/page.tsx index 77f3cf8..c381c03 100644 --- a/src/app/home/page.tsx +++ b/src/app/(main)/home/page.tsx @@ -1,15 +1,13 @@ 'use client'; import { RedirectButton } from '@/components/buttons/redirect-button'; -import { ThemePicker } from '@/components/misc/theme-picker'; import { useGetApiUserMe } from '@/generated/api/user/user'; export default function Home() { const { data, isLoading } = useGetApiUserMe(); return ( -
-
{}
+

Hello{' '} diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx new file mode 100644 index 0000000..7106e70 --- /dev/null +++ b/src/app/(main)/layout.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { cookies } from 'next/headers'; + +import { AppSidebar } from '@/components/custom-ui/app-sidebar'; +import SidebarProviderWrapper from '@/components/wrappers/sidebar-provider'; +import Header from '@/components/misc/header'; + +export default async function Layout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + const cookieStore = await cookies(); + const defaultOpen = cookieStore.get('sidebar_state')?.value === 'true'; + return ( + <> + + +
{children}
+
+ + ); +} diff --git a/src/app/globals.css b/src/app/globals.css index f85cb2f..a5f7eaf 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -55,6 +55,8 @@ --card: var(--neutral-800); + --sidebar-width-icon: 32px; + /* ------------------- */ --foreground: oklch(0.13 0.028 261.692); @@ -95,17 +97,17 @@ --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0.002 247.839); + --sidebar: var(--background); - --sidebar-foreground: oklch(0.13 0.028 261.692); + --sidebar-foreground: var(--text); --sidebar-primary: oklch(0.21 0.034 264.665); - --sidebar-primary-foreground: oklch(0.985 0.002 247.839); + --sidebar-primary-foreground: var(--text); --sidebar-accent: oklch(0.967 0.003 264.542); - --sidebar-accent-foreground: oklch(0.21 0.034 264.665); + --sidebar-accent-foreground: var(--text); --sidebar-border: oklch(0.928 0.006 264.531); @@ -339,17 +341,17 @@ --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.21 0.034 264.665); + --sidebar: var(--background); - --sidebar-foreground: oklch(0.985 0.002 247.839); + --sidebar-foreground: var(--text); --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0.002 247.839); + --sidebar-primary-foreground: var(--text); --sidebar-accent: oklch(0.278 0.033 256.848); - --sidebar-accent-foreground: oklch(0.985 0.002 247.839); + --sidebar-accent-foreground: var(--text); --sidebar-border: oklch(1 0 0 / 10%); diff --git a/src/components/custom-ui/app-sidebar.tsx b/src/components/custom-ui/app-sidebar.tsx new file mode 100644 index 0000000..b279c73 --- /dev/null +++ b/src/components/custom-ui/app-sidebar.tsx @@ -0,0 +1,147 @@ +'use client'; + +import React from 'react'; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupAction, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarInput, + SidebarInset, + SidebarMenu, + SidebarMenuAction, + SidebarMenuBadge, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSkeleton, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, + SidebarProvider, + SidebarRail, + SidebarSeparator, + SidebarTrigger, + useSidebar, +} from '@/components/ui/sidebar'; + +import { ChevronDown } from 'lucide-react'; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@/components/ui/collapsible'; + +import Logo from '@/components/misc/logo'; + +import Link from 'next/link'; + +import { ThemePicker } from '@/components/misc/theme-picker'; + +import { + Star, + CalendarDays, + User, + Users, + CalendarClock, + CalendarPlus, +} from 'lucide-react'; + +const items = [ + { + title: 'Calendar', + url: '#', + icon: CalendarDays, + }, + { + title: 'Friends', + url: '#', + icon: User, + }, + { + title: 'Groups', + url: '#', + icon: Users, + }, + { + title: 'Events', + url: '#', + icon: CalendarClock, + }, +]; + +export function AppSidebar() { + return ( + <> + + + + + + + + + + + + {' '} + + Favorites + + + + + + + + + + + + + {items.map((item) => ( + + + + + + {item.title} + + + + + ))} + + + + + + + + + New Event + + + + + + + + ); +} diff --git a/src/components/misc/header.tsx b/src/components/misc/header.tsx new file mode 100644 index 0000000..dbf8a1f --- /dev/null +++ b/src/components/misc/header.tsx @@ -0,0 +1,23 @@ +import { SidebarTrigger } from '@/components/ui/sidebar'; +import { ThemePicker } from '@/components/misc/theme-picker'; + +export default function Header({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( +
+
+ + + + Search + + + +
+
{children}
+
+ ); +} diff --git a/src/components/misc/logo.tsx b/src/components/misc/logo.tsx index 129adef..739fc90 100644 --- a/src/components/misc/logo.tsx +++ b/src/components/misc/logo.tsx @@ -90,6 +90,7 @@ export default function Logo({ return ( {alt) { + return ; +} + +function CollapsibleTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CollapsibleContent({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Collapsible, CollapsibleTrigger, CollapsibleContent }; diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx index 3b4f1ef..3234cdc 100644 --- a/src/components/ui/separator.tsx +++ b/src/components/ui/separator.tsx @@ -13,10 +13,13 @@ function Separator({ }: React.ComponentProps) { return ( ); diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx new file mode 100644 index 0000000..84649ad --- /dev/null +++ b/src/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/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx new file mode 100644 index 0000000..6b68b8f --- /dev/null +++ b/src/components/ui/sidebar.tsx @@ -0,0 +1,725 @@ +'use client'; + +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, VariantProps } 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 = '4rem'; +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 ( + + + + + ); +} diff --git a/src/components/custom-ui/app-sidebar.tsx b/src/components/custom-ui/app-sidebar.tsx index ae00c9b..4861363 100644 --- a/src/components/custom-ui/app-sidebar.tsx +++ b/src/components/custom-ui/app-sidebar.tsx @@ -6,27 +6,27 @@ import { SidebarContent, SidebarFooter, SidebarGroup, - SidebarGroupAction, + // SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, - SidebarInput, - SidebarInset, + // SidebarInput, + // SidebarInset, SidebarMenu, - SidebarMenuAction, - SidebarMenuBadge, + // SidebarMenuAction, + // SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, - SidebarMenuSkeleton, - SidebarMenuSub, - SidebarMenuSubButton, - SidebarMenuSubItem, - SidebarProvider, - SidebarRail, - SidebarSeparator, - SidebarTrigger, - useSidebar, -} from '@/components/ui/sidebar'; + // SidebarMenuSkeleton, + // SidebarMenuSub, + // SidebarMenuSubButton, + // SidebarMenuSubItem, + // SidebarProvider, + // SidebarRail, + // SidebarSeparator, + // SidebarTrigger, + // useSidebar, +} from '@/components/custom-ui/sidebar'; import { ChevronDown } from 'lucide-react'; import { @@ -39,8 +39,6 @@ import Logo from '@/components/misc/logo'; import Link from 'next/link'; -import { ThemePicker } from '@/components/misc/theme-picker'; - import { Star, CalendarDays, diff --git a/src/components/ui/sidebar.tsx b/src/components/custom-ui/sidebar.tsx similarity index 99% rename from src/components/ui/sidebar.tsx rename to src/components/custom-ui/sidebar.tsx index 6b68b8f..11228cb 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/components/custom-ui/sidebar.tsx @@ -466,7 +466,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) {
  • ); diff --git a/src/components/misc/header.tsx b/src/components/misc/header.tsx index dbf8a1f..ed53953 100644 --- a/src/components/misc/header.tsx +++ b/src/components/misc/header.tsx @@ -1,5 +1,22 @@ -import { SidebarTrigger } from '@/components/ui/sidebar'; +import { SidebarTrigger } from '@/components/custom-ui/sidebar'; import { ThemePicker } from '@/components/misc/theme-picker'; +import { NotificationButton } from '@/components/buttons/notification-button'; + +import { BellRing, Inbox } from 'lucide-react'; +import UserDropdown from '@/components/misc/user-dropdown'; + +const items = [ + { + title: 'Calendar', + url: '#', + icon: Inbox, + }, + { + title: 'Friends', + url: '#', + icon: BellRing, + }, +]; export default function Header({ children, @@ -8,13 +25,24 @@ export default function Header({ }>) { return (
    -
    +
    Search - + + {items.map((item) => ( + + + + ))} +
    {children}
    diff --git a/src/components/misc/nav-user.tsx b/src/components/misc/nav-user.tsx new file mode 100644 index 0000000..53ab582 --- /dev/null +++ b/src/components/misc/nav-user.tsx @@ -0,0 +1,110 @@ +'use client'; + +import { + BadgeCheck, + Bell, + ChevronsUpDown, + CreditCard, + LogOut, + Sparkles, +} 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/custom-ui/sidebar'; + +export function NavUser({ + user, +}: { + user: { + name: string; + email: string; + avatar: string; + }; +}) { + const { isMobile } = useSidebar(); + + return ( + + + + + + + + CN + +
    + {user.name} + {user.email} +
    + +
    +
    + + +
    + + + CN + +
    + {user.name} + {user.email} +
    +
    +
    + + + + + Upgrade to Pro + + + + + + + Account + + + + Billing + + + + Notifications + + + + + + Log out + +
    +
    +
    +
    + ); +} diff --git a/src/components/misc/notification-dot.tsx b/src/components/misc/notification-dot.tsx new file mode 100644 index 0000000..a918188 --- /dev/null +++ b/src/components/misc/notification-dot.tsx @@ -0,0 +1,35 @@ +import { cn } from '@/lib/utils'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { CircleSmall } from 'lucide-react'; + +const dotVariants = cva('', { + variants: { + variant: { + neutral: 'fill-neutral-900', + active: 'fill-red-600 stroke-red-600', + hidden: 'hidden', + }, + }, + defaultVariants: { + variant: 'hidden', + }, +}); + +function NotificationDot({ + className, + dotVariant, + ...props +}: { + className: string; + dotVariant: VariantProps['variant']; +}) { + return ( + + ); +} + +export type NDot = VariantProps['variant']; +export { NotificationDot, dotVariants }; diff --git a/src/components/misc/user-card.tsx b/src/components/misc/user-card.tsx new file mode 100644 index 0000000..faefc35 --- /dev/null +++ b/src/components/misc/user-card.tsx @@ -0,0 +1,29 @@ +import { useGetApiUserMe } from '@/generated/api/user/user'; +import { Avatar } from '@/components/ui/avatar'; +import Image from 'next/image'; +import { User } from 'lucide-react'; + +export default function UserCard() { + const { data } = useGetApiUserMe(); + return ( +
    + + {data?.data.user.image ? ( + Avatar + ) : ( + + )} + +
    {data?.data.user.name}
    +
    + {data?.data.user.email} +
    +
    + ); +} diff --git a/src/components/misc/user-dropdown.tsx b/src/components/misc/user-dropdown.tsx new file mode 100644 index 0000000..8f5aa05 --- /dev/null +++ b/src/components/misc/user-dropdown.tsx @@ -0,0 +1,59 @@ +'use client'; + +import { Avatar } from '@/components/ui/avatar'; +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + // DropdownMenuLabel, + // DropdownMenuPortal, + DropdownMenuSeparator, + // DropdownMenuShortcut, + // DropdownMenuSub, + // DropdownMenuSubContent, + // DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { useGetApiUserMe } from '@/generated/api/user/user'; +import { ChevronDown, User } from 'lucide-react'; +import Image from 'next/image'; +import Link from 'next/link'; +import UserCard from '@/components/misc/user-card'; + +export default function UserDropdown() { + const { data } = useGetApiUserMe(); + return ( + + + + + + + + + + Settings + + + Logout + + + + ); +} diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx new file mode 100644 index 0000000..6a21b65 --- /dev/null +++ b/src/components/ui/avatar.tsx @@ -0,0 +1,53 @@ +'use client'; + +import * as React from 'react'; +import * as AvatarPrimitive from '@radix-ui/react-avatar'; + +import { cn } from '@/lib/utils'; + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx index 84649ad..e8d5ec1 100644 --- a/src/components/ui/sheet.tsx +++ b/src/components/ui/sheet.tsx @@ -1,31 +1,31 @@ -"use client" +'use client'; -import * as React from "react" -import * as SheetPrimitive from "@radix-ui/react-dialog" -import { XIcon } from "lucide-react" +import * as React from 'react'; +import * as SheetPrimitive from '@radix-ui/react-dialog'; +import { XIcon } from 'lucide-react'; -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils'; function Sheet({ ...props }: React.ComponentProps) { - return + return ; } function SheetTrigger({ ...props }: React.ComponentProps) { - return + return ; } function SheetClose({ ...props }: React.ComponentProps) { - return + return ; } function SheetPortal({ ...props }: React.ComponentProps) { - return + return ; } function SheetOverlay({ @@ -34,71 +34,71 @@ function SheetOverlay({ }: React.ComponentProps) { return ( - ) + ); } function SheetContent({ className, children, - side = "right", + side = 'right', ...props }: React.ComponentProps & { - side?: "top" | "right" | "bottom" | "left" + side?: 'top' | 'right' | 'bottom' | 'left'; }) { return ( {children} - - - Close + + + Close - ) + ); } -function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { +function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) { return (
    - ) + ); } -function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { +function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) { return (
    - ) + ); } function SheetTitle({ @@ -107,11 +107,11 @@ function SheetTitle({ }: React.ComponentProps) { return ( - ) + ); } function SheetDescription({ @@ -120,11 +120,11 @@ function SheetDescription({ }: React.ComponentProps) { return ( - ) + ); } export { @@ -136,4 +136,4 @@ export { SheetFooter, SheetTitle, SheetDescription, -} +}; diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx index 32ea0ef..a9344b2 100644 --- a/src/components/ui/skeleton.tsx +++ b/src/components/ui/skeleton.tsx @@ -1,13 +1,13 @@ -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils'; -function Skeleton({ className, ...props }: React.ComponentProps<"div">) { +function Skeleton({ className, ...props }: React.ComponentProps<'div'>) { return (
    - ) + ); } -export { Skeleton } +export { Skeleton }; diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx index 4ee26b3..2b8b1d7 100644 --- a/src/components/ui/tooltip.tsx +++ b/src/components/ui/tooltip.tsx @@ -1,9 +1,9 @@ -"use client" +'use client'; -import * as React from "react" -import * as TooltipPrimitive from "@radix-ui/react-tooltip" +import * as React from 'react'; +import * as TooltipPrimitive from '@radix-ui/react-tooltip'; -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils'; function TooltipProvider({ delayDuration = 0, @@ -11,11 +11,11 @@ function TooltipProvider({ }: React.ComponentProps) { return ( - ) + ); } function Tooltip({ @@ -23,15 +23,15 @@ function Tooltip({ }: React.ComponentProps) { return ( - + - ) + ); } function TooltipTrigger({ ...props }: React.ComponentProps) { - return + return ; } function TooltipContent({ @@ -43,19 +43,19 @@ function TooltipContent({ return ( {children} - + - ) + ); } -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/src/components/query-provider.tsx b/src/components/wrappers/query-provider.tsx similarity index 100% rename from src/components/query-provider.tsx rename to src/components/wrappers/query-provider.tsx diff --git a/src/components/wrappers/sidebar-provider.tsx b/src/components/wrappers/sidebar-provider.tsx index 3873fa4..3c9ff95 100644 --- a/src/components/wrappers/sidebar-provider.tsx +++ b/src/components/wrappers/sidebar-provider.tsx @@ -1,7 +1,7 @@ 'use client'; import React from 'react'; -import { SidebarProvider } from '../ui/sidebar'; +import { SidebarProvider } from '../custom-ui/sidebar'; export default function SidebarProviderWrapper({ defaultOpen, diff --git a/src/hooks/use-mobile.ts b/src/hooks/use-mobile.ts index 2b0fe1d..821f8ff 100644 --- a/src/hooks/use-mobile.ts +++ b/src/hooks/use-mobile.ts @@ -1,19 +1,21 @@ -import * as React from "react" +import * as React from 'react'; -const MOBILE_BREAKPOINT = 768 +const MOBILE_BREAKPOINT = 768; export function useIsMobile() { - const [isMobile, setIsMobile] = React.useState(undefined) + const [isMobile, setIsMobile] = React.useState( + undefined, + ); React.useEffect(() => { - const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); const onChange = () => { - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) - } - mql.addEventListener("change", onChange) - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) - return () => mql.removeEventListener("change", onChange) - }, []) + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + }; + mql.addEventListener('change', onChange); + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + return () => mql.removeEventListener('change', onChange); + }, []); - return !!isMobile + return !!isMobile; } diff --git a/yarn.lock b/yarn.lock index e1ff28c..90dc258 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1299,6 +1299,29 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-avatar@npm:^1.1.10": + version: 1.1.10 + resolution: "@radix-ui/react-avatar@npm:1.1.10" + dependencies: + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-callback-ref": "npm:1.1.1" + "@radix-ui/react-use-is-hydrated": "npm:0.1.0" + "@radix-ui/react-use-layout-effect": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/9fb0cf9a9d0fdbeaa2efda476402fc09db2e6ff9cd9aa3ea1d315d9c9579840722a4833725cb196c455e0bd775dfe04221a4f6855685ce89d2133c42e2b07e5f + languageName: node + linkType: hard + "@radix-ui/react-collapsible@npm:^1.1.11": version: 1.1.11 resolution: "@radix-ui/react-collapsible@npm:1.1.11" @@ -1441,7 +1464,7 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-dropdown-menu@npm:^2.1.14": +"@radix-ui/react-dropdown-menu@npm:^2.1.15": version: 2.1.15 resolution: "@radix-ui/react-dropdown-menu@npm:2.1.15" dependencies: @@ -1951,6 +1974,21 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-use-is-hydrated@npm:0.1.0": + version: 0.1.0 + resolution: "@radix-ui/react-use-is-hydrated@npm:0.1.0" + dependencies: + use-sync-external-store: "npm:^1.5.0" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/635079bafe32829fc7405895154568ea94a22689b170489fd6d77668e4885e72ff71ed6d0ea3d602852841ef0f1927aa400fee2178d5dfbeb8bc9297da7d6498 + languageName: node + linkType: hard + "@radix-ui/react-use-layout-effect@npm:1.1.1": version: 1.1.1 resolution: "@radix-ui/react-use-layout-effect@npm:1.1.1" @@ -6637,9 +6675,10 @@ __metadata: "@fortawesome/react-fontawesome": "npm:^0.2.2" "@hookform/resolvers": "npm:^5.0.1" "@prisma/client": "npm:^6.9.0" + "@radix-ui/react-avatar": "npm:^1.1.10" "@radix-ui/react-collapsible": "npm:^1.1.11" "@radix-ui/react-dialog": "npm:^1.1.14" - "@radix-ui/react-dropdown-menu": "npm:^2.1.14" + "@radix-ui/react-dropdown-menu": "npm:^2.1.15" "@radix-ui/react-hover-card": "npm:^1.1.13" "@radix-ui/react-label": "npm:^2.1.6" "@radix-ui/react-scroll-area": "npm:^1.2.8" @@ -9385,7 +9424,7 @@ __metadata: languageName: node linkType: hard -"use-sync-external-store@npm:^1.4.0": +"use-sync-external-store@npm:^1.4.0, use-sync-external-store@npm:^1.5.0": version: 1.5.0 resolution: "use-sync-external-store@npm:1.5.0" peerDependencies: From bc1910fdcacfb43f0bab84fc71b261cd2af115d4 Mon Sep 17 00:00:00 2001 From: SomeCodecat <88855796+SomeCodecat@users.noreply.github.com> Date: Mon, 23 Jun 2025 11:20:09 +0200 Subject: [PATCH 4/8] feat: Implement settings dropdown and page components - Added `SettingsDropdown` component for selecting settings sections with icons and descriptions. - Created `SettingsPage` component to manage user settings, including account details, notifications, calendar availability, privacy, and appearance. - Introduced `SettingsSwitcher` for selecting options within settings. - Integrated command and dialog components for improved user interaction. - Updated `UserDropdown` to include links for settings and logout. - Refactored button styles and card footer layout for consistency. - Added popover functionality for dropdown menus. - Updated dependencies in `yarn.lock` for new components. --- package.json | 2 + src/app/settings/page.tsx | 483 +------------------- src/components/misc/settings-dropdown.tsx | 158 +++++++ src/components/misc/settings-page.tsx | 467 +++++++++++++++++++ src/components/misc/settings-switcher.tsx | 123 +++++ src/components/misc/user-card.tsx | 2 +- src/components/misc/user-dropdown.tsx | 12 +- src/components/ui/button.tsx | 2 +- src/components/ui/card.tsx | 2 +- src/components/ui/command.tsx | 184 ++++++++ src/components/ui/dialog.tsx | 143 ++++++ src/components/ui/popover.tsx | 48 ++ src/components/wrappers/settings-scroll.tsx | 20 +- yarn.lock | 58 ++- 14 files changed, 1202 insertions(+), 502 deletions(-) create mode 100644 src/components/misc/settings-dropdown.tsx create mode 100644 src/components/misc/settings-page.tsx create mode 100644 src/components/misc/settings-switcher.tsx create mode 100644 src/components/ui/command.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/components/ui/popover.tsx diff --git a/package.json b/package.json index a9aa812..e3e8883 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-hover-card": "^1.1.13", "@radix-ui/react-label": "^2.1.6", + "@radix-ui/react-popover": "^1.1.14", "@radix-ui/react-scroll-area": "^1.2.8", "@radix-ui/react-select": "^2.2.4", "@radix-ui/react-separator": "^1.1.7", @@ -44,6 +45,7 @@ "bcryptjs": "^3.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "lucide-react": "^0.515.0", "next": "15.4.0-canary.92", "next-auth": "^5.0.0-beta.25", diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 563ebab..e0ad2a5 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -1,482 +1,5 @@ -import { Button } from '@/components/ui/button'; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from '@/components/ui/card'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { ScrollableSettingsWrapper } from '@/components/wrappers/settings-scroll'; -import { Switch } from '@/components/ui/switch'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; +import SettingsPage from '@/components/misc/settings-page'; -export default function SettingsPage() { - return ( -
    -
    - - - Account - Notifications - Calendar - Privacy - Appearance - - - - - - - Account Settings - - Manage your account details and preferences. - - - -
    - - -
    -
    - - -

    - Email is managed by your SSO provider. -

    -
    -
    - - -

    - Upload a new profile picture. -

    -
    -
    - - -
    - -
    - - -
    -
    - -

    - Permanently delete your account and all associated data. -

    -
    -
    -
    - - - - -
    -
    - - - - - - Notification Preferences - - Choose how you want to be notified. - - - -
    - - -
    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    -
    - - - - -
    -
    - - - - - - Calendar & Availability - - Manage your calendar display, default availability, and iCal - integrations. - - - -
    - - Display - -
    - - -
    -
    - - -
    -
    - - -
    -
    - -
    - - Availability - -
    - -

    - Define your typical available hours (e.g., - Monday-Friday, 9 AM - 5 PM). -

    - -
    -
    - -

    - Min time before a booking can be made. -

    -
    - -
    -
    -
    - -

    - Max time in advance a booking can be made. -

    - -
    -
    - -
    - - iCalendar Integration - -
    - - - -
    -
    - - - -
    -
    -
    -
    - - - - -
    -
    - - - - - - Sharing & Privacy - - Control who can see your calendar and book time with you. - - - -
    - - -
    -
    - -

    - (Override for Default Visibility) -
    - - This setting will override the default visibility for - your calendar. You can set specific friends or groups to - see your full calendar details. - -

    - -
    -
    - - -
    -
    - - -

    - Prevent specific users from seeing your calendar or - booking time. -

    -
    -
    -
    - - - - -
    -
    - - - - - - Appearance - - Customize the look and feel of the application. - - - -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    - - - - -
    -
    -
    -
    -
    - ); +export default function Page() { + return ; } diff --git a/src/components/misc/settings-dropdown.tsx b/src/components/misc/settings-dropdown.tsx new file mode 100644 index 0000000..63a7b20 --- /dev/null +++ b/src/components/misc/settings-dropdown.tsx @@ -0,0 +1,158 @@ +'use client'; + +import type React from 'react'; + +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; +import { cn } from '@/lib/utils'; +import { + Check, + ChevronDown, + User, + Bell, + Calendar, + Shield, + Palette, +} from 'lucide-react'; + +interface SettingsSection { + label: string; + value: string; + description: string; + icon: React.ComponentType<{ className?: string }>; +} + +interface SettingsDropdownProps { + currentSection: string; + onSectionChange: (section: string) => void; + className?: string; +} + +const settingsSections: SettingsSection[] = [ + { + label: 'Account', + value: 'general', + description: 'Manage your account details and preferences', + icon: User, + }, + { + label: 'Notifications', + value: 'notifications', + description: 'Choose how you want to be notified', + icon: Bell, + }, + { + label: 'Calendar', + value: 'calendarAvailability', + description: 'Manage calendar display and availability', + icon: Calendar, + }, + { + label: 'Privacy', + value: 'sharingPrivacy', + description: 'Control who can see your calendar', + icon: Shield, + }, + { + label: 'Appearance', + value: 'appearance', + description: 'Customize the look and feel', + icon: Palette, + }, +]; + +export function SettingsDropdown({ + currentSection, + onSectionChange, + className, +}: SettingsDropdownProps) { + const [open, setOpen] = useState(false); + + const currentSectionData = settingsSections.find( + (section) => section.value === currentSection, + ); + const CurrentIcon = currentSectionData?.icon || User; + + const handleSelect = (value: string) => { + onSectionChange(value); + setOpen(false); + }; + + return ( +
    + + + + + + + + + No settings found. + + {settingsSections.map((section) => { + const Icon = section.icon; + return ( + handleSelect(section.value)} + className='flex items-center justify-between p-3' + > +
    + +
    + {section.label} + + {section.description} + +
    +
    + +
    + ); + })} +
    +
    +
    +
    +
    +
    + ); +} diff --git a/src/components/misc/settings-page.tsx b/src/components/misc/settings-page.tsx new file mode 100644 index 0000000..fb90614 --- /dev/null +++ b/src/components/misc/settings-page.tsx @@ -0,0 +1,467 @@ +'use client'; + +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { ScrollableSettingsWrapper } from '@/components/wrappers/settings-scroll'; +import { Switch } from '@/components/ui/switch'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { SettingsDropdown } from '@/components/misc/settings-dropdown'; +import { useRouter } from 'next/navigation'; + +export default function SettingsPage() { + const router = useRouter(); + const [currentSection, setCurrentSection] = useState('general'); + + const renderSettingsContent = () => { + switch (currentSection) { + case 'general': + return ( + + + + Account Settings + + Manage your account details and preferences. + + + +
    + + +
    +
    + + +

    + Email is managed by your SSO provider. +

    +
    +
    + + +

    + Upload a new profile picture. +

    +
    +
    + + +
    +
    + + +
    +
    + +

    + Permanently delete your account and all associated data. +

    +
    +
    +
    +
    + ); + + case 'notifications': + return ( + + + + Notification Preferences + + Choose how you want to be notified. + + + +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    + ); + + case 'calendarAvailability': + return ( + + + + Calendar & Availability + + Manage your calendar display, default availability, and iCal + integrations. + + + +
    + Display +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    + + Availability + +
    + +

    + Define your typical available hours (e.g., Monday-Friday, + 9 AM - 5 PM). +

    + +
    +
    + +

    + Min time before a booking can be made. +

    +
    + +
    +
    +
    + +

    + Max time in advance a booking can be made. +

    + +
    +
    + +
    + + iCalendar Integration + +
    + + + +
    +
    + + + +
    +
    +
    +
    +
    + ); + + case 'sharingPrivacy': + return ( + + + + Sharing & Privacy + + Control who can see your calendar and book time with you. + + + +
    + + +
    +
    + +

    + (Override for Default Visibility) +
    + + This setting will override the default visibility for your + calendar. You can set specific friends or groups to see + your full calendar details. + +

    + +
    +
    + + +
    +
    + + +

    + Prevent specific users from seeing your calendar or booking + time. +

    +
    +
    +
    +
    + ); + + case 'appearance': + return ( + + + + Appearance + + Customize the look and feel of the application. + + + +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    + ); + + default: + return null; + } + }; + + return ( +
    +
    +
    +
    +

    Settings

    +
    + +
    + +
    {renderSettingsContent()}
    +
    + + + + +
    +
    +
    + ); +} diff --git a/src/components/misc/settings-switcher.tsx b/src/components/misc/settings-switcher.tsx new file mode 100644 index 0000000..40e6a05 --- /dev/null +++ b/src/components/misc/settings-switcher.tsx @@ -0,0 +1,123 @@ +'use client'; + +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; +import { cn } from '@/lib/utils'; +import { Check, ChevronDown } from 'lucide-react'; + +interface SettingsOption { + label: string; + value: string; + description?: string; +} + +interface SettingsSwitcherProps { + title: string; + options: SettingsOption[]; + defaultValue?: string; + onValueChange?: (value: string) => void; + placeholder?: string; + searchPlaceholder?: string; + className?: string; +} + +export function SettingsSwitcher({ + title, + options, + defaultValue, + onValueChange, + placeholder = 'Select option...', + searchPlaceholder = 'Search options...', + className, +}: SettingsSwitcherProps) { + const [open, setOpen] = useState(false); + const [selectedValue, setSelectedValue] = useState( + defaultValue || options[0]?.value || '', + ); + + const selectedOption = options.find( + (option) => option.value === selectedValue, + ); + + const handleSelect = (value: string) => { + setSelectedValue(value); + setOpen(false); + onValueChange?.(value); + }; + + return ( +
    + + + + + + + + + + No option found. + + {options.map((option) => ( + handleSelect(option.value)} + className='flex items-center justify-between' + > +
    + {option.label} + {option.description && ( + + {option.description} + + )} +
    + +
    + ))} +
    +
    +
    +
    +
    +
    + ); +} diff --git a/src/components/misc/user-card.tsx b/src/components/misc/user-card.tsx index faefc35..457d0fc 100644 --- a/src/components/misc/user-card.tsx +++ b/src/components/misc/user-card.tsx @@ -21,7 +21,7 @@ export default function UserCard() { )}
    {data?.data.user.name}
    -
    +
    {data?.data.user.email}
    diff --git a/src/components/misc/user-dropdown.tsx b/src/components/misc/user-dropdown.tsx index 8f5aa05..c0cc68a 100644 --- a/src/components/misc/user-dropdown.tsx +++ b/src/components/misc/user-dropdown.tsx @@ -5,7 +5,7 @@ import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, - DropdownMenuGroup, + // DropdownMenuGroup, DropdownMenuItem, // DropdownMenuLabel, // DropdownMenuPortal, @@ -48,11 +48,13 @@ export default function UserDropdown() { - Settings + + Settings + - - Logout - + + Logout + ); diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 2a25f66..0b8ce1a 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -5,7 +5,7 @@ import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils'; const buttonVariants = cva( - "radius-lg inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-button transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + "radius-lg inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-button transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-nonef", { variants: { variant: { diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index c0894f8..72c0a9c 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -126,7 +126,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { return (
    ); diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx new file mode 100644 index 0000000..8cb4ca7 --- /dev/null +++ b/src/components/ui/command.tsx @@ -0,0 +1,184 @@ +"use client" + +import * as React from "react" +import { Command as CommandPrimitive } from "cmdk" +import { SearchIcon } from "lucide-react" + +import { cn } from "@/lib/utils" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" + +function Command({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandDialog({ + title = "Command Palette", + description = "Search for a command to run...", + children, + className, + showCloseButton = true, + ...props +}: React.ComponentProps & { + title?: string + description?: string + className?: string + showCloseButton?: boolean +}) { + return ( + + + {title} + {description} + + + + {children} + + + + ) +} + +function CommandInput({ + className, + ...props +}: React.ComponentProps) { + return ( +
    + + +
    + ) +} + +function CommandList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandEmpty({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..d9ccec9 --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,143 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 0000000..01e468b --- /dev/null +++ b/src/components/ui/popover.tsx @@ -0,0 +1,48 @@ +"use client" + +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +function Popover({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverContent({ + className, + align = "center", + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function PopoverAnchor({ + ...props +}: React.ComponentProps) { + return +} + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } diff --git a/src/components/wrappers/settings-scroll.tsx b/src/components/wrappers/settings-scroll.tsx index e0f7251..a647493 100644 --- a/src/components/wrappers/settings-scroll.tsx +++ b/src/components/wrappers/settings-scroll.tsx @@ -1,16 +1,16 @@ -import React from 'react'; +import { cn } from '@/lib/utils'; +import type * as React from 'react'; -interface ScrollableContentWrapperProps { - children: React.ReactNode; +interface ScrollableSettingsWrapperProps { className?: string; + children: React.ReactNode; } -export const ScrollableSettingsWrapper: React.FC< - ScrollableContentWrapperProps -> = ({ children, className = '' }) => { +export function ScrollableSettingsWrapper({ + className, + children, +}: ScrollableSettingsWrapperProps) { return ( -
    - {children} -
    +
    {children}
    ); -}; +} diff --git a/yarn.lock b/yarn.lock index 90dc258..7d3866e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1370,7 +1370,7 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-compose-refs@npm:1.1.2": +"@radix-ui/react-compose-refs@npm:1.1.2, @radix-ui/react-compose-refs@npm:^1.1.1": version: 1.1.2 resolution: "@radix-ui/react-compose-refs@npm:1.1.2" peerDependencies: @@ -1396,7 +1396,7 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-dialog@npm:^1.1.14": +"@radix-ui/react-dialog@npm:^1.1.14, @radix-ui/react-dialog@npm:^1.1.6": version: 1.1.14 resolution: "@radix-ui/react-dialog@npm:1.1.14" dependencies: @@ -1550,7 +1550,7 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-id@npm:1.1.1": +"@radix-ui/react-id@npm:1.1.1, @radix-ui/react-id@npm:^1.1.0": version: 1.1.1 resolution: "@radix-ui/react-id@npm:1.1.1" dependencies: @@ -1620,6 +1620,39 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-popover@npm:^1.1.14": + version: 1.1.14 + resolution: "@radix-ui/react-popover@npm:1.1.14" + dependencies: + "@radix-ui/primitive": "npm:1.1.2" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-dismissable-layer": "npm:1.1.10" + "@radix-ui/react-focus-guards": "npm:1.1.2" + "@radix-ui/react-focus-scope": "npm:1.1.7" + "@radix-ui/react-id": "npm:1.1.1" + "@radix-ui/react-popper": "npm:1.2.7" + "@radix-ui/react-portal": "npm:1.1.9" + "@radix-ui/react-presence": "npm:1.1.4" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-slot": "npm:1.2.3" + "@radix-ui/react-use-controllable-state": "npm:1.2.2" + aria-hidden: "npm:^1.2.4" + react-remove-scroll: "npm:^2.6.3" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/04e557bfcaab4887694d119555b101e16b8a4e99595541ff2cbe805c551be853cb02882a2ada04e6507ffc45bc092bc2b89704b7b79f5025251767d0b4f3230a + languageName: node + linkType: hard + "@radix-ui/react-popper@npm:1.2.7": version: 1.2.7 resolution: "@radix-ui/react-popper@npm:1.2.7" @@ -1688,7 +1721,7 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-primitive@npm:2.1.3": +"@radix-ui/react-primitive@npm:2.1.3, @radix-ui/react-primitive@npm:^2.0.2": version: 2.1.3 resolution: "@radix-ui/react-primitive@npm:2.1.3" dependencies: @@ -4184,6 +4217,21 @@ __metadata: languageName: node linkType: hard +"cmdk@npm:^1.1.1": + version: 1.1.1 + resolution: "cmdk@npm:1.1.1" + dependencies: + "@radix-ui/react-compose-refs": "npm:^1.1.1" + "@radix-ui/react-dialog": "npm:^1.1.6" + "@radix-ui/react-id": "npm:^1.1.0" + "@radix-ui/react-primitive": "npm:^2.0.2" + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + checksum: 10c0/5605ac4396ec9bc65c82f954da19dd89a0636a54026df72780e2470da1381f9d57434a80a53f2d57eaa4e759660a3ebba9232b74258dc09970576591eae03116 + languageName: node + linkType: hard + "color-convert@npm:^2.0.1": version: 2.0.1 resolution: "color-convert@npm:2.0.1" @@ -6681,6 +6729,7 @@ __metadata: "@radix-ui/react-dropdown-menu": "npm:^2.1.15" "@radix-ui/react-hover-card": "npm:^1.1.13" "@radix-ui/react-label": "npm:^2.1.6" + "@radix-ui/react-popover": "npm:^1.1.14" "@radix-ui/react-scroll-area": "npm:^1.2.8" "@radix-ui/react-select": "npm:^2.2.4" "@radix-ui/react-separator": "npm:^1.1.7" @@ -6698,6 +6747,7 @@ __metadata: bcryptjs: "npm:^3.0.2" class-variance-authority: "npm:^0.7.1" clsx: "npm:^2.1.1" + cmdk: "npm:^1.1.1" dotenv-cli: "npm:8.0.0" eslint: "npm:9.29.0" eslint-config-next: "npm:15.3.4" From 7f5f8642ef341e169f0027de9907c7b03abcac85 Mon Sep 17 00:00:00 2001 From: SomeCodecat <88855796+SomeCodecat@users.noreply.github.com> Date: Tue, 24 Jun 2025 11:45:50 +0200 Subject: [PATCH 5/8] feat: tempcommit --- src/components/custom-ui/app-sidebar.tsx | 4 +- src/components/misc/settings-dropdown.tsx | 8 +- src/components/misc/settings-page.tsx | 128 ++++++++++++++-------- src/components/misc/settings-switcher.tsx | 123 --------------------- src/components/misc/user-dropdown.tsx | 1 + src/components/wrappers/group-wrapper.tsx | 23 ++++ 6 files changed, 113 insertions(+), 174 deletions(-) delete mode 100644 src/components/misc/settings-switcher.tsx create mode 100644 src/components/wrappers/group-wrapper.tsx diff --git a/src/components/custom-ui/app-sidebar.tsx b/src/components/custom-ui/app-sidebar.tsx index 4861363..d7958e4 100644 --- a/src/components/custom-ui/app-sidebar.tsx +++ b/src/components/custom-ui/app-sidebar.tsx @@ -66,7 +66,7 @@ const items = [ }, { title: 'Events', - url: '#', + url: '/events', icon: CalendarClock, }, ]; @@ -128,7 +128,7 @@ export function AppSidebar() { diff --git a/src/components/misc/settings-dropdown.tsx b/src/components/misc/settings-dropdown.tsx index 63a7b20..5bf256c 100644 --- a/src/components/misc/settings-dropdown.tsx +++ b/src/components/misc/settings-dropdown.tsx @@ -57,19 +57,19 @@ const settingsSections: SettingsSection[] = [ { label: 'Calendar', value: 'calendarAvailability', - description: 'Manage calendar display and availability', + description: 'Manage calendar display, availability and iCal integration', icon: Calendar, }, { label: 'Privacy', value: 'sharingPrivacy', - description: 'Control who can see your calendar', + description: 'Control who can see your calendar and book time with you', icon: Shield, }, { label: 'Appearance', value: 'appearance', - description: 'Customize the look and feel', + description: 'Customize the look and feel of the application', icon: Palette, }, ]; @@ -99,7 +99,7 @@ export function SettingsDropdown({ variant='outline_muted' role='combobox' aria-expanded={open} - className='w-full justify-between bg-white text-black h-auto py-3' + className='w-full justify-between bg-popover text-text h-auto py-3' >
    diff --git a/src/components/misc/settings-page.tsx b/src/components/misc/settings-page.tsx index fb90614..ce91579 100644 --- a/src/components/misc/settings-page.tsx +++ b/src/components/misc/settings-page.tsx @@ -23,10 +23,15 @@ import { } from '@/components/ui/select'; import { SettingsDropdown } from '@/components/misc/settings-dropdown'; import { useRouter } from 'next/navigation'; +import { useGetApiUserMe } from '@/generated/api/user/user'; +import { ThemePicker } from './theme-picker'; +import LabeledInput from '../custom-ui/labeled-input'; +import { GroupWrapper } from '../wrappers/group-wrapper'; export default function SettingsPage() { const router = useRouter(); const [currentSection, setCurrentSection] = useState('general'); + const { data } = useGetApiUserMe(); const renderSettingsContent = () => { switch (currentSection) { @@ -36,28 +41,79 @@ export default function SettingsPage() { Account Settings - - Manage your account details and preferences. - - -
    - - -
    -
    - - -

    - Email is managed by your SSO provider. -

    -
    + + +
    +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    + + +

    + Email might be managed by your SSO provider. +

    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    @@ -67,7 +123,11 @@ export default function SettingsPage() {
    - +
    @@ -98,9 +158,6 @@ export default function SettingsPage() { Notification Preferences - - Choose how you want to be notified. -
    @@ -175,10 +232,6 @@ export default function SettingsPage() { Calendar & Availability - - Manage your calendar display, default availability, and iCal - integrations. -
    @@ -298,9 +351,6 @@ export default function SettingsPage() { Sharing & Privacy - - Control who can see your calendar and book time with you. -
    @@ -386,23 +436,11 @@ export default function SettingsPage() { Appearance - - Customize the look and feel of the application. -
    - +
    diff --git a/src/components/misc/settings-switcher.tsx b/src/components/misc/settings-switcher.tsx deleted file mode 100644 index 40e6a05..0000000 --- a/src/components/misc/settings-switcher.tsx +++ /dev/null @@ -1,123 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from '@/components/ui/command'; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover'; -import { cn } from '@/lib/utils'; -import { Check, ChevronDown } from 'lucide-react'; - -interface SettingsOption { - label: string; - value: string; - description?: string; -} - -interface SettingsSwitcherProps { - title: string; - options: SettingsOption[]; - defaultValue?: string; - onValueChange?: (value: string) => void; - placeholder?: string; - searchPlaceholder?: string; - className?: string; -} - -export function SettingsSwitcher({ - title, - options, - defaultValue, - onValueChange, - placeholder = 'Select option...', - searchPlaceholder = 'Search options...', - className, -}: SettingsSwitcherProps) { - const [open, setOpen] = useState(false); - const [selectedValue, setSelectedValue] = useState( - defaultValue || options[0]?.value || '', - ); - - const selectedOption = options.find( - (option) => option.value === selectedValue, - ); - - const handleSelect = (value: string) => { - setSelectedValue(value); - setOpen(false); - onValueChange?.(value); - }; - - return ( -
    - - - - - - - - - - No option found. - - {options.map((option) => ( - handleSelect(option.value)} - className='flex items-center justify-between' - > -
    - {option.label} - {option.description && ( - - {option.description} - - )} -
    - -
    - ))} -
    -
    -
    -
    -
    -
    - ); -} diff --git a/src/components/misc/user-dropdown.tsx b/src/components/misc/user-dropdown.tsx index c0cc68a..8220d06 100644 --- a/src/components/misc/user-dropdown.tsx +++ b/src/components/misc/user-dropdown.tsx @@ -24,6 +24,7 @@ import UserCard from '@/components/misc/user-card'; export default function UserDropdown() { const { data } = useGetApiUserMe(); + return ( diff --git a/src/components/wrappers/group-wrapper.tsx b/src/components/wrappers/group-wrapper.tsx new file mode 100644 index 0000000..713b5d6 --- /dev/null +++ b/src/components/wrappers/group-wrapper.tsx @@ -0,0 +1,23 @@ +import { cn } from '@/lib/utils'; +import type * as React from 'react'; + +interface ScrollableSettingsWrapperProps { + className?: string; + legend?: string; + children: React.ReactNode; +} + +export function GroupWrapper({ + className, + legend, + children, +}: ScrollableSettingsWrapperProps) { + return ( +
    + {legend} + {children} +
    + ); +} From 7691bd2face5b01efdbd8a1821d193af59705170 Mon Sep 17 00:00:00 2001 From: SomeCodecat <88855796+SomeCodecat@users.noreply.github.com> Date: Wed, 25 Jun 2025 11:52:00 +0200 Subject: [PATCH 6/8] feat: tempcommit --- src/components/misc/settings-dropdown.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/misc/settings-dropdown.tsx b/src/components/misc/settings-dropdown.tsx index 5bf256c..c49629c 100644 --- a/src/components/misc/settings-dropdown.tsx +++ b/src/components/misc/settings-dropdown.tsx @@ -105,9 +105,9 @@ export function SettingsDropdown({
    {currentSectionData?.label} - +

    {currentSectionData?.description} - +

    @@ -132,9 +132,9 @@ export function SettingsDropdown({
    {section.label} - +

    {section.description} - +

    Date: Thu, 26 Jun 2025 20:25:13 +0200 Subject: [PATCH 7/8] feat: tempcommit --- src/app/globals.css | 105 ++++++++++++++++++-- src/components/custom-ui/labeled-input.tsx | 39 +++++--- src/components/misc/settings-dropdown.tsx | 4 +- src/components/misc/settings-page.tsx | 107 ++++++++++++--------- src/components/wrappers/group-wrapper.tsx | 6 +- 5 files changed, 190 insertions(+), 71 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 93a24ce..bc18178 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -9,6 +9,7 @@ --font-heading: 'Comfortaa', sans-serif; --font-label: 'Varela Round', sans-serif; --font-button: 'Varela Round', sans-serif; + --font-sans: var(--font-label); --transparent: transparent; @@ -28,7 +29,7 @@ --background: var(--neutral-800); --background-reversed: var(--neutral-000); - --base: var(--neutral-800); + --basecl: var(--neutral-800); --text: var(--neutral-000); --text-alt: var(--neutral-900); --text-input: var(--text); @@ -49,11 +50,23 @@ --active-secondary: oklch(0.4254 0.133 272.15); --disabled-secondary: oklch(0.4937 0.1697 271.26 / 0.5); + --destructive: oklch(60.699% 0.20755 25.945); + --hover-destructive: oklch(60.699% 0.20755 25.945 / 0.8); + --active-destructive: oklch(50.329% 0.17084 25.842); + --disabled-destructive: oklch(60.699% 0.20755 25.945 / 0.4); + --muted: var(--color-neutral-700); --hover-muted: var(--color-neutral-600); --active-muted: var(--color-neutral-400); --disabled-muted: var(--color-neutral-400); + --toaster-default-bg: var(--color-neutral-150); + --toaster-success-bg: oklch(54.147% 0.09184 144.208); + --toaster-error-bg: oklch(52.841% 0.10236 27.274); + --toaster-info-bg: oklch(44.298% 0.05515 259.369); + --toaster-warning-bg: oklch(61.891% 0.07539 102.943); + --toaster-notification-bg: var(--color-neutral-150); + --card: var(--neutral-800); --sidebar-width-icon: 32px; @@ -80,8 +93,6 @@ --accent-foreground: oklch(0.21 0.034 264.665); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.928 0.006 264.531); --input: oklch(0.928 0.006 264.531); @@ -115,6 +126,62 @@ --sidebar-ring: oklch(0.707 0.022 261.325); } +h1 { + font-family: var(--font-heading); + font-size: 40px; + font-style: normal; + font-weight: 700; + line-height: normal; +} + +h2 { + font-family: var(--font-heading); + font-size: 36px; + font-style: normal; + font-weight: 600; + line-height: normal; +} + +h3 { + font-family: var(--font-heading); + font-size: 32px; + font-style: normal; + font-weight: 600; + line-height: normal; +} + +h4 { + font-family: var(--font-heading); + font-size: 28px; + font-style: normal; + font-weight: 600; + line-height: normal; +} + +h5 { + font-family: var(--font-heading); + font-size: 26px; + font-style: normal; + font-weight: 600; + line-height: normal; +} + +h6 { + font-family: var(--font-heading); + font-size: 20px; + font-style: normal; + font-weight: 600; + line-height: normal; +} + +p { + font-family: var(--font-label); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; +} + @font-face { font-family: 'Comfortaa'; font-style: normal; @@ -153,7 +220,7 @@ --color-background: var(--neutral-750); --color-background-reversed: var(--background-reversed); - --color-base: var(--neutral-800); + --color-basecl: var(--neutral-800); --color-text: var(--text); --color-text-alt: var(--text-alt); --color-text-input: var(--text-input); @@ -175,11 +242,23 @@ --color-active-secondary: var(--active-secondary); --color-disabled-secondary: var(--disabled-secondary); + --color-destructive: var(--destructive); + --color-hover-destructive: var(--hover-destructive); + --color-active-destructive: var(--active-destructive); + --color-disabled-destructive: var(--disabled-destructive); + --color-muted: var(--muted); --color-hover-muted: var(--hover-muted); --color-active-muted: var(--active-muted); --color-disabled-muted: var(--disabled-muted); + --color-toaster-default-bg: var(--toaster-default-bg); + --color-toaster-success-bg: var(--toaster-success-bg); + --color-toaster-error-bg: var(--toaster-error-bg); + --color-toaster-info-bg: var(--toaster-info-bg); + --color-toaster-warning-bg: var(--toaster-warning-bg); + --color-toaster-notification-bg: var(--toaster-notification-bg); + /* Custom values */ --radius-sm: calc(var(--radius) - 4px); @@ -220,8 +299,6 @@ --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-border: var(--border); --color-input: var(--input); @@ -277,7 +354,7 @@ --background: var(--neutral-750); --background-reversed: var(--neutral-000); - --base: var(--neutral-750); + --basecl: var(--neutral-750); --text: var(--neutral-000); --text-alt: var(--neutral-900); --text-input: var(--text); @@ -297,11 +374,23 @@ --active-secondary: oklch(0.4471 0.15 271.61); --disabled-secondary: oklch(0.6065 0.213 271.11 / 0.4); + --destructive: oklch(0.58 0.2149 27.13); + --hover-destructive: oklch(0.58 0.2149 27.13 / 0.8); + --active-destructive: oklch(45.872% 0.16648 26.855); + --disabled-destructive: oklch(0.58 0.2149 27.13 / 0.4); + --muted: var(--color-neutral-650); --hover-muted: var(--color-neutral-500); --active-muted: var(--color-neutral-400); --disabled-muted: var(--color-neutral-400); + --toaster-default-bg: var(--color-neutral-150); + --toaster-success-bg: var(--color-green-200); + --toaster-error-bg: var(--color-red-200); + --toaster-info-bg: var(--color-blue-200); + --toaster-warning-bg: var(--color-yellow-200); + --toaster-notification-bg: var(--color-neutral-150); + --card: var(--neutral-750); /* ------------------- */ @@ -326,8 +415,6 @@ --accent-foreground: oklch(0.985 0.002 247.839); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); diff --git a/src/components/custom-ui/labeled-input.tsx b/src/components/custom-ui/labeled-input.tsx index ea26e51..b83524d 100644 --- a/src/components/custom-ui/labeled-input.tsx +++ b/src/components/custom-ui/labeled-input.tsx @@ -1,5 +1,8 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import React from 'react'; +import { Button } from '../ui/button'; +import { Eye, EyeOff } from 'lucide-react'; export default function LabeledInput({ type, @@ -11,7 +14,7 @@ export default function LabeledInput({ error, ...rest }: { - type: 'text' | 'email' | 'password'; + type: 'text' | 'email' | 'password' | 'file'; label: string; placeholder?: string; value?: string; @@ -19,19 +22,33 @@ export default function LabeledInput({ autocomplete?: string; error?: string; } & React.InputHTMLAttributes) { + const [passwordVisible, setPasswordVisible] = React.useState(false); + return (
    - - + + + {type === 'password' && ( + + )} + {error &&

    {error}

    }
    ); diff --git a/src/components/misc/settings-dropdown.tsx b/src/components/misc/settings-dropdown.tsx index c49629c..6eaae8d 100644 --- a/src/components/misc/settings-dropdown.tsx +++ b/src/components/misc/settings-dropdown.tsx @@ -45,13 +45,13 @@ const settingsSections: SettingsSection[] = [ { label: 'Account', value: 'general', - description: 'Manage your account details and preferences', + description: 'Manage account details', icon: User, }, { label: 'Notifications', value: 'notifications', - description: 'Choose how you want to be notified', + description: 'Choose notification Preferences', icon: Bell, }, { diff --git a/src/components/misc/settings-page.tsx b/src/components/misc/settings-page.tsx index ce91579..34e1dd0 100644 --- a/src/components/misc/settings-page.tsx +++ b/src/components/misc/settings-page.tsx @@ -27,6 +27,9 @@ import { useGetApiUserMe } from '@/generated/api/user/user'; import { ThemePicker } from './theme-picker'; import LabeledInput from '../custom-ui/labeled-input'; import { GroupWrapper } from '../wrappers/group-wrapper'; +import { Avatar } from '../ui/avatar'; +import Image from 'next/image'; +import { User } from 'lucide-react'; export default function SettingsPage() { const router = useRouter(); @@ -43,51 +46,50 @@ export default function SettingsPage() { Account Settings - +
    - - + >
    - - + >
    - - + >
    -
    - - + -

    + label='Email Address' + placeholder='Your E-Mail' + defaultValue={data?.data.user.email ?? ''} + > + + Email might be managed by your SSO provider. -

    +
    - -
    + +
    -
    - - -

    - Upload a new profile picture. -

    +
    + + + {data?.data.user.image ? ( + Avatar + ) : ( + + )} +
    @@ -143,9 +158,9 @@ export default function SettingsPage() {
    -

    + Permanently delete your account and all associated data. -

    +
    @@ -277,10 +292,10 @@ export default function SettingsPage() {
    -

    + Define your typical available hours (e.g., Monday-Friday, 9 AM - 5 PM). -

    + @@ -289,9 +304,9 @@ export default function SettingsPage() { -

    + Min time before a booking can be made. -

    +
    Booking Window (days in advance) -

    + Max time in advance a booking can be made. -

    + Who Can See Your Full Calendar Details? -

    + (Override for Default Visibility)
    @@ -386,7 +401,7 @@ export default function SettingsPage() { calendar. You can set specific friends or groups to see your full calendar details. -

    + + + {data?.data.user.image ? ( + Avatar + ) : ( + + )} + + +
    + + ); +} diff --git a/src/components/misc/settings-page.tsx b/src/components/misc/settings-page.tsx index 34e1dd0..33a083b 100644 --- a/src/components/misc/settings-page.tsx +++ b/src/components/misc/settings-page.tsx @@ -30,6 +30,7 @@ import { GroupWrapper } from '../wrappers/group-wrapper'; import { Avatar } from '../ui/avatar'; import Image from 'next/image'; import { User } from 'lucide-react'; +import ProfilePictureUpload from './profile-picture-upload'; export default function SettingsPage() { const router = useRouter(); @@ -48,24 +49,26 @@ export default function SettingsPage() {
    -
    -
    - + +
    +
    + +
    +
    + +
    -
    - -
    -
    +
    -
    - - - {data?.data.user.image ? ( - Avatar - ) : ( - - )} - -
    -
    - - -
    -
    - - -
    -
    + +
    + +
    +
    + +
    +
    + +
    +
    +
    + + +
    +
    +
    +
    +
    Permanently delete your account and all associated data.