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 (
+ <>
+
+
+
+
+ >
+ );
+}
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 (
+
+
+ {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 (
) {
+ 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 (
+ {
+ onClick?.(event);
+ toggleSidebar();
+ }}
+ {...props}
+ >
+
+ Toggle Sidebar
+
+ );
+}
+
+function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) {
+ const { toggleSidebar } = useSidebar();
+
+ return (
+
+ );
+}
+
+function SidebarInset({ className, ...props }: React.ComponentProps<'main'>) {
+ return (
+
+ );
+}
+
+function SidebarInput({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function SidebarHeader({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function SidebarFooter({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function SidebarSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function SidebarContent({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function SidebarGroup({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function SidebarGroupLabel({
+ className,
+ asChild = false,
+ ...props
+}: React.ComponentProps<'div'> & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : 'div';
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0 ml-[7.5px]',
+ className,
+ )}
+ {...props}
+ />
+ );
+}
+
+function SidebarGroupAction({
+ className,
+ asChild = false,
+ ...props
+}: React.ComponentProps<'button'> & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : 'button';
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0',
+ // Increases the hit area of the button on mobile.
+ 'after:absolute after:-inset-2 md:after:hidden',
+ 'group-data-[collapsible=icon]:hidden',
+ className,
+ )}
+ {...props}
+ />
+ );
+}
+
+function SidebarGroupContent({
+ className,
+ ...props
+}: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function SidebarMenu({ className, ...props }: React.ComponentProps<'ul'>) {
+ return (
+
+ );
+}
+
+function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) {
+ return (
+
+ );
+}
+
+const sidebarMenuButtonVariants = cva(
+ 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! p-0 ml-[15.5px] [&>span:last-child]:truncate [&>svg]:shrink-0',
+ {
+ variants: {
+ variant: {
+ default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
+ outline:
+ 'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',
+ },
+ size: {
+ default: 'h-8 text-sm',
+ sm: 'h-7 text-xs',
+ lg: 'h-12 text-sm group-data-[collapsible=icon]:p-0!',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default',
+ },
+ },
+);
+
+function SidebarMenuButton({
+ asChild = false,
+ isActive = false,
+ variant = 'default',
+ size = 'default',
+ tooltip,
+ className,
+ ...props
+}: React.ComponentProps<'button'> & {
+ asChild?: boolean;
+ isActive?: boolean;
+ tooltip?: string | React.ComponentProps;
+} & VariantProps) {
+ const Comp = asChild ? Slot : 'button';
+ const { isMobile, state } = useSidebar();
+
+ const button = (
+
+ );
+
+ if (!tooltip) {
+ return button;
+ }
+
+ if (typeof tooltip === 'string') {
+ tooltip = {
+ children: tooltip,
+ };
+ }
+
+ return (
+
+ {button}
+
+
+ );
+}
+
+function SidebarMenuAction({
+ className,
+ asChild = false,
+ showOnHover = false,
+ ...props
+}: React.ComponentProps<'button'> & {
+ asChild?: boolean;
+ showOnHover?: boolean;
+}) {
+ const Comp = asChild ? Slot : 'button';
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0',
+ // Increases the hit area of the button on mobile.
+ 'after:absolute after:-inset-2 md:after:hidden',
+ 'peer-data-[size=sm]/menu-button:top-1',
+ 'peer-data-[size=default]/menu-button:top-1.5',
+ 'peer-data-[size=lg]/menu-button:top-2.5',
+ 'group-data-[collapsible=icon]:hidden',
+ showOnHover &&
+ 'peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0',
+ className,
+ )}
+ {...props}
+ />
+ );
+}
+
+function SidebarMenuBadge({
+ className,
+ ...props
+}: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function SidebarMenuSkeleton({
+ className,
+ showIcon = false,
+ ...props
+}: React.ComponentProps<'div'> & {
+ showIcon?: boolean;
+}) {
+ // Random width between 50 to 90%.
+ const width = React.useMemo(() => {
+ return `${Math.floor(Math.random() * 40) + 50}%`;
+ }, []);
+
+ return (
+
+ {showIcon && (
+
+ )}
+
+
+ );
+}
+
+function SidebarMenuSub({ className, ...props }: React.ComponentProps<'ul'>) {
+ return (
+
+ );
+}
+
+function SidebarMenuSubItem({
+ className,
+ ...props
+}: React.ComponentProps<'li'>) {
+ return (
+
+ );
+}
+
+function SidebarMenuSubButton({
+ asChild = false,
+ size = 'md',
+ isActive = false,
+ className,
+ ...props
+}: React.ComponentProps<'a'> & {
+ asChild?: boolean;
+ size?: 'sm' | 'md';
+ isActive?: boolean;
+}) {
+ const Comp = asChild ? Slot : 'a';
+
+ return (
+ svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
+ 'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
+ size === 'sm' && 'text-xs',
+ size === 'md' && 'text-sm',
+ 'group-data-[collapsible=icon]:hidden',
+ className,
+ )}
+ {...props}
+ />
+ );
+}
+
+export {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupAction,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarInput,
+ SidebarInset,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuBadge,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarMenuSkeleton,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem,
+ SidebarProvider,
+ SidebarRail,
+ SidebarSeparator,
+ SidebarTrigger,
+ useSidebar,
+};
diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx
new file mode 100644
index 0000000..32ea0ef
--- /dev/null
+++ b/src/components/ui/skeleton.tsx
@@ -0,0 +1,13 @@
+import { cn } from "@/lib/utils"
+
+function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export { Skeleton }
diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx
new file mode 100644
index 0000000..4ee26b3
--- /dev/null
+++ b/src/components/ui/tooltip.tsx
@@ -0,0 +1,61 @@
+"use client"
+
+import * as React from "react"
+import * as TooltipPrimitive from "@radix-ui/react-tooltip"
+
+import { cn } from "@/lib/utils"
+
+function TooltipProvider({
+ delayDuration = 0,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function Tooltip({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function TooltipTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function TooltipContent({
+ className,
+ sideOffset = 0,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+ )
+}
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
diff --git a/src/components/wrappers/sidebar-provider.tsx b/src/components/wrappers/sidebar-provider.tsx
new file mode 100644
index 0000000..3873fa4
--- /dev/null
+++ b/src/components/wrappers/sidebar-provider.tsx
@@ -0,0 +1,23 @@
+'use client';
+
+import React from 'react';
+import { SidebarProvider } from '../ui/sidebar';
+
+export default function SidebarProviderWrapper({
+ defaultOpen,
+ children,
+}: {
+ defaultOpen: boolean;
+ children: React.ReactNode;
+}) {
+ const [open, setOpen] = React.useState(defaultOpen);
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/hooks/use-mobile.ts b/src/hooks/use-mobile.ts
new file mode 100644
index 0000000..2b0fe1d
--- /dev/null
+++ b/src/hooks/use-mobile.ts
@@ -0,0 +1,19 @@
+import * as React from "react"
+
+const MOBILE_BREAKPOINT = 768
+
+export function useIsMobile() {
+ const [isMobile, setIsMobile] = React.useState(undefined)
+
+ React.useEffect(() => {
+ 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)
+ }, [])
+
+ return !!isMobile
+}
diff --git a/yarn.lock b/yarn.lock
index 56de1d1..e1ff28c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -543,15 +543,15 @@ __metadata:
linkType: hard
"@gerrit0/mini-shiki@npm:^3.2.2":
- version: 3.6.0
- resolution: "@gerrit0/mini-shiki@npm:3.6.0"
+ version: 3.7.0
+ resolution: "@gerrit0/mini-shiki@npm:3.7.0"
dependencies:
- "@shikijs/engine-oniguruma": "npm:^3.6.0"
- "@shikijs/langs": "npm:^3.6.0"
- "@shikijs/themes": "npm:^3.6.0"
- "@shikijs/types": "npm:^3.6.0"
+ "@shikijs/engine-oniguruma": "npm:^3.7.0"
+ "@shikijs/langs": "npm:^3.7.0"
+ "@shikijs/themes": "npm:^3.7.0"
+ "@shikijs/types": "npm:^3.7.0"
"@shikijs/vscode-textmate": "npm:^10.0.2"
- checksum: 10c0/347456c9da8a1fadd3c1f63097da459a5f930ef4bca6431cce913a379012c551e061d0a94ff7a0f307215b87f2418b7c198a55fba888fc97fb02ab36247adf6b
+ checksum: 10c0/eb3f4900d841338077d839ebbc7f8722b13876a586cff7abc73295e956683724dd3371a9f990900184a2d069461965951b2604d677991badf3474262e7811384
languageName: node
linkType: hard
@@ -1299,6 +1299,32 @@ __metadata:
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"
+ 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-id": "npm:1.1.1"
+ "@radix-ui/react-presence": "npm:1.1.4"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@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/fa2de539ef06e2b2d18acebb12a34ce1534ca88bd484b7359aac05534d1e551fe83eaafbf60915c00161bb370f0dc9fc303903133510dea0a59fd018155b7db5
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-collection@npm:1.1.7":
version: 1.1.7
resolution: "@radix-ui/react-collection@npm:1.1.7"
@@ -1347,6 +1373,38 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-dialog@npm:^1.1.14":
+ version: 1.1.14
+ resolution: "@radix-ui/react-dialog@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-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/ab7bc783510ed8fccfe91020b214f4a571d5a1d46d398faa33f4c151bc9f586c47483b307e72b67687b06694c194b3aa80dd1de728460fa765db9f3057690ba3
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-direction@npm:1.1.1":
version: 1.1.1
resolution: "@radix-ui/react-direction@npm:1.1.1"
@@ -1719,7 +1777,7 @@ __metadata:
languageName: node
linkType: hard
-"@radix-ui/react-separator@npm:^1.1.6":
+"@radix-ui/react-separator@npm:^1.1.7":
version: 1.1.7
resolution: "@radix-ui/react-separator@npm:1.1.7"
dependencies:
@@ -1738,7 +1796,7 @@ __metadata:
languageName: node
linkType: hard
-"@radix-ui/react-slot@npm:1.2.3, @radix-ui/react-slot@npm:^1.2.2":
+"@radix-ui/react-slot@npm:1.2.3, @radix-ui/react-slot@npm:^1.2.3":
version: 1.2.3
resolution: "@radix-ui/react-slot@npm:1.2.3"
dependencies:
@@ -1804,6 +1862,36 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-tooltip@npm:^1.2.7":
+ version: 1.2.7
+ resolution: "@radix-ui/react-tooltip@npm:1.2.7"
+ 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-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"
+ "@radix-ui/react-visually-hidden": "npm:1.2.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/28798d576c6ffec4f11120cd563aa9d5ab9afb9a37dc18778176442756d026c8c46eec1ddc647b2b5914045495fcb89f82530106e91acb55776b7d6b1a10fb57
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-use-callback-ref@npm:1.1.1":
version: 1.1.1
resolution: "@radix-ui/react-use-callback-ref@npm:1.1.1"
@@ -1966,7 +2054,7 @@ __metadata:
languageName: node
linkType: hard
-"@shikijs/engine-oniguruma@npm:^3.6.0":
+"@shikijs/engine-oniguruma@npm:^3.7.0":
version: 3.7.0
resolution: "@shikijs/engine-oniguruma@npm:3.7.0"
dependencies:
@@ -1976,7 +2064,7 @@ __metadata:
languageName: node
linkType: hard
-"@shikijs/langs@npm:^3.6.0":
+"@shikijs/langs@npm:^3.7.0":
version: 3.7.0
resolution: "@shikijs/langs@npm:3.7.0"
dependencies:
@@ -1985,7 +2073,7 @@ __metadata:
languageName: node
linkType: hard
-"@shikijs/themes@npm:^3.6.0":
+"@shikijs/themes@npm:^3.7.0":
version: 3.7.0
resolution: "@shikijs/themes@npm:3.7.0"
dependencies:
@@ -1994,7 +2082,7 @@ __metadata:
languageName: node
linkType: hard
-"@shikijs/types@npm:3.7.0, @shikijs/types@npm:^3.6.0":
+"@shikijs/types@npm:3.7.0, @shikijs/types@npm:^3.7.0":
version: 3.7.0
resolution: "@shikijs/types@npm:3.7.0"
dependencies:
@@ -3662,7 +3750,7 @@ __metadata:
languageName: node
linkType: hard
-"array-includes@npm:^3.1.6, array-includes@npm:^3.1.8":
+"array-includes@npm:^3.1.6, array-includes@npm:^3.1.8, array-includes@npm:^3.1.9":
version: 3.1.9
resolution: "array-includes@npm:3.1.9"
dependencies:
@@ -3699,7 +3787,7 @@ __metadata:
languageName: node
linkType: hard
-"array.prototype.findlastindex@npm:^1.2.5":
+"array.prototype.findlastindex@npm:^1.2.6":
version: 1.2.6
resolution: "array.prototype.findlastindex@npm:1.2.6"
dependencies:
@@ -3714,7 +3802,7 @@ __metadata:
languageName: node
linkType: hard
-"array.prototype.flat@npm:^1.3.1, array.prototype.flat@npm:^1.3.2":
+"array.prototype.flat@npm:^1.3.1, array.prototype.flat@npm:^1.3.3":
version: 1.3.3
resolution: "array.prototype.flat@npm:1.3.3"
dependencies:
@@ -4792,7 +4880,7 @@ __metadata:
languageName: node
linkType: hard
-"eslint-module-utils@npm:^2.12.0":
+"eslint-module-utils@npm:^2.12.1":
version: 2.12.1
resolution: "eslint-module-utils@npm:2.12.1"
dependencies:
@@ -4805,31 +4893,31 @@ __metadata:
linkType: hard
"eslint-plugin-import@npm:^2.31.0":
- version: 2.31.0
- resolution: "eslint-plugin-import@npm:2.31.0"
+ version: 2.32.0
+ resolution: "eslint-plugin-import@npm:2.32.0"
dependencies:
"@rtsao/scc": "npm:^1.1.0"
- array-includes: "npm:^3.1.8"
- array.prototype.findlastindex: "npm:^1.2.5"
- array.prototype.flat: "npm:^1.3.2"
- array.prototype.flatmap: "npm:^1.3.2"
+ array-includes: "npm:^3.1.9"
+ array.prototype.findlastindex: "npm:^1.2.6"
+ array.prototype.flat: "npm:^1.3.3"
+ array.prototype.flatmap: "npm:^1.3.3"
debug: "npm:^3.2.7"
doctrine: "npm:^2.1.0"
eslint-import-resolver-node: "npm:^0.3.9"
- eslint-module-utils: "npm:^2.12.0"
+ eslint-module-utils: "npm:^2.12.1"
hasown: "npm:^2.0.2"
- is-core-module: "npm:^2.15.1"
+ is-core-module: "npm:^2.16.1"
is-glob: "npm:^4.0.3"
minimatch: "npm:^3.1.2"
object.fromentries: "npm:^2.0.8"
object.groupby: "npm:^1.0.3"
- object.values: "npm:^1.2.0"
+ object.values: "npm:^1.2.1"
semver: "npm:^6.3.1"
- string.prototype.trimend: "npm:^1.0.8"
+ string.prototype.trimend: "npm:^1.0.9"
tsconfig-paths: "npm:^3.15.0"
peerDependencies:
eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9
- checksum: 10c0/e21d116ddd1900e091ad120b3eb68c5dd5437fe2c930f1211781cd38b246f090a6b74d5f3800b8255a0ed29782591521ad44eb21c5534960a8f1fb4040fd913a
+ checksum: 10c0/bfb1b8fc8800398e62ddfefbf3638d185286edfed26dfe00875cc2846d954491b4f5112457831588b757fa789384e1ae585f812614c4797f0499fa234fd4a48b
languageName: node
linkType: hard
@@ -5758,7 +5846,7 @@ __metadata:
languageName: node
linkType: hard
-"is-core-module@npm:^2.13.0, is-core-module@npm:^2.15.1, is-core-module@npm:^2.16.0":
+"is-core-module@npm:^2.13.0, is-core-module@npm:^2.16.0, is-core-module@npm:^2.16.1":
version: 2.16.1
resolution: "is-core-module@npm:2.16.1"
dependencies:
@@ -6454,12 +6542,12 @@ __metadata:
languageName: node
linkType: hard
-"lucide-react@npm:^0.511.0":
- version: 0.511.0
- resolution: "lucide-react@npm:0.511.0"
+"lucide-react@npm:^0.515.0":
+ version: 0.515.0
+ resolution: "lucide-react@npm:0.515.0"
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
- checksum: 10c0/bf09dd73cf2233abea90506ad31a91739555d761062722acbe045cb73e274f035b196472de0971a8a8f0645b2b54e3f21b8c1980fe87c909ca93171a9c28428a
+ checksum: 10c0/00485e09ab3d0bbb34797b1f368c269e8708522b6e2f46fd84dd5bd99741546487be9a65a260f274e8049b81cc37687566e26132f5752352c8d9bc8e5d0b3dea
languageName: node
linkType: hard
@@ -6549,15 +6637,18 @@ __metadata:
"@fortawesome/react-fontawesome": "npm:^0.2.2"
"@hookform/resolvers": "npm:^5.0.1"
"@prisma/client": "npm:^6.9.0"
+ "@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-hover-card": "npm:^1.1.13"
"@radix-ui/react-label": "npm:^2.1.6"
"@radix-ui/react-scroll-area": "npm:^1.2.8"
"@radix-ui/react-select": "npm:^2.2.4"
- "@radix-ui/react-separator": "npm:^1.1.6"
- "@radix-ui/react-slot": "npm:^1.2.2"
+ "@radix-ui/react-separator": "npm:^1.1.7"
+ "@radix-ui/react-slot": "npm:^1.2.3"
"@radix-ui/react-switch": "npm:^1.2.4"
"@radix-ui/react-tabs": "npm:^1.1.11"
+ "@radix-ui/react-tooltip": "npm:^1.2.7"
"@tailwindcss/postcss": "npm:4.1.10"
"@tanstack/react-query": "npm:^5.80.7"
"@types/node": "npm:22.15.32"
@@ -6572,7 +6663,7 @@ __metadata:
eslint: "npm:9.29.0"
eslint-config-next: "npm:15.3.4"
eslint-config-prettier: "npm:10.1.5"
- lucide-react: "npm:^0.511.0"
+ lucide-react: "npm:^0.515.0"
next: "npm:15.4.0-canary.92"
next-auth: "npm:^5.0.0-beta.25"
next-themes: "npm:^0.4.6"
@@ -7185,7 +7276,7 @@ __metadata:
languageName: node
linkType: hard
-"object.values@npm:^1.1.6, object.values@npm:^1.2.0, object.values@npm:^1.2.1":
+"object.values@npm:^1.1.6, object.values@npm:^1.2.1":
version: 1.2.1
resolution: "object.values@npm:1.2.1"
dependencies:
@@ -8569,7 +8660,7 @@ __metadata:
languageName: node
linkType: hard
-"string.prototype.trimend@npm:^1.0.8, string.prototype.trimend@npm:^1.0.9":
+"string.prototype.trimend@npm:^1.0.9":
version: 1.0.9
resolution: "string.prototype.trimend@npm:1.0.9"
dependencies:
From 15015931d6ce0ca090b382aaf69d4977dd83f9b0 Mon Sep 17 00:00:00 2001
From: Micha
Date: Sun, 22 Jun 2025 22:39:46 +0200
Subject: [PATCH 2/8] fix: add new Logos for equal hight in Sidebar
---
src/assets/logo/logo-export.ts | 8 ++++----
src/assets/logo/new/logo_colored_combo_dark.svg | 17 +++++++++++++++++
.../logo/new/logo_colored_combo_light.svg | 17 +++++++++++++++++
.../logo/new/logo_colored_submark_dark.svg | 10 ++++++++++
.../logo/new/logo_colored_submark_light.svg | 10 ++++++++++
src/components/custom-ui/app-sidebar.tsx | 6 +++---
6 files changed, 61 insertions(+), 7 deletions(-)
create mode 100644 src/assets/logo/new/logo_colored_combo_dark.svg
create mode 100644 src/assets/logo/new/logo_colored_combo_light.svg
create mode 100644 src/assets/logo/new/logo_colored_submark_dark.svg
create mode 100644 src/assets/logo/new/logo_colored_submark_light.svg
diff --git a/src/assets/logo/logo-export.ts b/src/assets/logo/logo-export.ts
index 44681d3..17a0708 100644
--- a/src/assets/logo/logo-export.ts
+++ b/src/assets/logo/logo-export.ts
@@ -1,5 +1,5 @@
-export { default as logo_colored_combo_light } from '@/assets/logo/logo_colored_combo_light.svg';
-export { default as logo_colored_combo_dark } from '@/assets/logo/logo_colored_combo_dark.svg';
+export { default as logo_colored_combo_light } from '@/assets/logo/new/logo_colored_combo_light.svg';
+export { default as logo_colored_combo_dark } from '@/assets/logo/new/logo_colored_combo_dark.svg';
export { default as logo_colored_primary_light } from '@/assets/logo/logo_colored_primary_light.svg';
export { default as logo_colored_primary_dark } from '@/assets/logo/logo_colored_primary_dark.svg';
export { default as logo_colored_secondary_light } from '@/assets/logo/logo_colored_secondary_light.svg';
@@ -12,5 +12,5 @@ export { default as logo_mono_secondary_light } from '@/assets/logo/logo_mono_se
export { default as logo_mono_secondary_dark } from '@/assets/logo/logo_mono_secondary_dark.svg';
export { default as logo_mono_submark_light } from '@/assets/logo/logo_mono_submark_light.svg';
export { default as logo_mono_submark_dark } from '@/assets/logo/logo_mono_submark_dark.svg';
-export { default as logo_colored_submark_light } from '@/assets/logo/logo_colored_submark_light.svg';
-export { default as logo_colored_submark_dark } from '@/assets/logo/logo_colored_submark_dark.svg';
+export { default as logo_colored_submark_light } from '@/assets/logo/new/logo_colored_submark_light.svg';
+export { default as logo_colored_submark_dark } from '@/assets/logo/new/logo_colored_submark_dark.svg';
diff --git a/src/assets/logo/new/logo_colored_combo_dark.svg b/src/assets/logo/new/logo_colored_combo_dark.svg
new file mode 100644
index 0000000..ec4acae
--- /dev/null
+++ b/src/assets/logo/new/logo_colored_combo_dark.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/logo/new/logo_colored_combo_light.svg b/src/assets/logo/new/logo_colored_combo_light.svg
new file mode 100644
index 0000000..33e6c6d
--- /dev/null
+++ b/src/assets/logo/new/logo_colored_combo_light.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/logo/new/logo_colored_submark_dark.svg b/src/assets/logo/new/logo_colored_submark_dark.svg
new file mode 100644
index 0000000..efadbf0
--- /dev/null
+++ b/src/assets/logo/new/logo_colored_submark_dark.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/logo/new/logo_colored_submark_light.svg b/src/assets/logo/new/logo_colored_submark_light.svg
new file mode 100644
index 0000000..cc7b3e1
--- /dev/null
+++ b/src/assets/logo/new/logo_colored_submark_light.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/custom-ui/app-sidebar.tsx b/src/components/custom-ui/app-sidebar.tsx
index b279c73..ae00c9b 100644
--- a/src/components/custom-ui/app-sidebar.tsx
+++ b/src/components/custom-ui/app-sidebar.tsx
@@ -81,14 +81,14 @@ export function AppSidebar() {
From 0cd88d6f07e58fffbb91a31a45e1d21e5a14e2d1 Mon Sep 17 00:00:00 2001
From: Maximilian Liebmann
Date: Sun, 22 Jun 2025 23:22:53 +0200
Subject: [PATCH 3/8] feat: enhance header with notification buttons and user
dropdown
- Updated header component to include notification buttons with icons.
- Introduced a new NavUser component for user-related actions in the sidebar.
- Added NotificationDot component for visual notification indicators.
- Created UserCard component to display user information.
- Implemented UserDropdown for user settings and logout functionality.
- Added Avatar component for user images with fallback support.
- Refactored Sheet and Tooltip components for consistency and improved styling.
- Introduced QueryProvider for managing React Query context.
- Updated SidebarProvider to use custom sidebar implementation.
- Enhanced mobile detection hook for better responsiveness.
- Updated dependencies in yarn.lock for new features and fixes.
feat: remove dot
---
package.json | 3 +-
src/app/globals.css | 3 +
src/app/layout.tsx | 2 +-
.../buttons/notification-button.tsx | 40 +++++++
src/components/custom-ui/app-sidebar.tsx | 32 +++--
src/components/{ui => custom-ui}/sidebar.tsx | 2 +-
src/components/misc/header.tsx | 34 +++++-
src/components/misc/nav-user.tsx | 110 ++++++++++++++++++
src/components/misc/notification-dot.tsx | 35 ++++++
src/components/misc/user-card.tsx | 29 +++++
src/components/misc/user-dropdown.tsx | 59 ++++++++++
src/components/ui/avatar.tsx | 53 +++++++++
src/components/ui/sheet.tsx | 90 +++++++-------
src/components/ui/skeleton.tsx | 12 +-
src/components/ui/tooltip.tsx | 30 ++---
.../{ => wrappers}/query-provider.tsx | 0
src/components/wrappers/sidebar-provider.tsx | 2 +-
src/hooks/use-mobile.ts | 24 ++--
yarn.lock | 45 ++++++-
19 files changed, 501 insertions(+), 104 deletions(-)
create mode 100644 src/components/buttons/notification-button.tsx
rename src/components/{ui => custom-ui}/sidebar.tsx (99%)
create mode 100644 src/components/misc/nav-user.tsx
create mode 100644 src/components/misc/notification-dot.tsx
create mode 100644 src/components/misc/user-card.tsx
create mode 100644 src/components/misc/user-dropdown.tsx
create mode 100644 src/components/ui/avatar.tsx
rename src/components/{ => wrappers}/query-provider.tsx (100%)
diff --git a/package.json b/package.json
index c1273c1..a9aa812 100644
--- a/package.json
+++ b/package.json
@@ -27,9 +27,10 @@
"@fortawesome/react-fontawesome": "^0.2.2",
"@hookform/resolvers": "^5.0.1",
"@prisma/client": "^6.9.0",
+ "@radix-ui/react-avatar": "^1.1.10",
"@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-dropdown-menu": "^2.1.15",
"@radix-ui/react-hover-card": "^1.1.13",
"@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-scroll-area": "^1.2.8",
diff --git a/src/app/globals.css b/src/app/globals.css
index a5f7eaf..93a24ce 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -33,6 +33,7 @@
--text-alt: var(--neutral-900);
--text-input: var(--text);
--text-muted-input: var(--neutral-450);
+ --text-muted: var(--neutral-300);
--muted-input: var(--neutral-600);
--background-disabled: var(--neutral-500);
--text-disabled: var(--neutral-700);
@@ -157,6 +158,7 @@
--color-text-alt: var(--text-alt);
--color-text-input: var(--text-input);
--color-text-muted-input: var(--text-muted-input);
+ --color-text-muted: var(--text-muted);
--color-muted-input: var(--muted-input);
--color-background-disabled: var(--neutral-500);
@@ -280,6 +282,7 @@
--text-alt: var(--neutral-900);
--text-input: var(--text);
--text-muted-input: var(--neutral-450);
+ --text-muted: var(--neutral-300);
--muted-input: var(--neutral-500);
--background-disabled: var(--neutral-500);
--text-disabled: var(--neutral-700);
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 201a730..af40867 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -2,7 +2,7 @@ import { ThemeProvider } from '@/components/wrappers/theme-provider';
import type { Metadata } from 'next';
import './globals.css';
-import { QueryProvider } from '@/components/query-provider';
+import { QueryProvider } from '@/components/wrappers/query-provider';
export const metadata: Metadata = {
title: 'MeetUp',
diff --git a/src/components/buttons/notification-button.tsx b/src/components/buttons/notification-button.tsx
new file mode 100644
index 0000000..0b718f9
--- /dev/null
+++ b/src/components/buttons/notification-button.tsx
@@ -0,0 +1,40 @@
+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 { NDot, NotificationDot } from '@/components/misc/notification-dot';
+
+export function NotificationButton({
+ dotVariant,
+ children,
+ ...props
+}: {
+ dotVariant?: NDot;
+ children: React.ReactNode;
+} & React.ComponentProps) {
+ return (
+
+
+
+ {children}
+
+
+
+
+
+ );
+}
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 ? (
+
+ ) : (
+
+ )}
+
+
{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 (
+
+
+
+
+ {data?.data.user.image ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+ 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.
-
-
-
-
- Display Name
-
-
-
-
Email Address
-
-
- Email is managed by your SSO provider.
-
-
-
-
Profile Picture
-
-
- Upload a new profile picture.
-
-
-
- Timezone
-
-
-
-
- Language
-
-
-
-
-
- English
- German
-
-
-
-
-
Delete Account
-
- Permanently delete your account and all associated data.
-
-
-
-
-
- Exit
- Save Changes
-
-
-
-
-
-
-
-
- Notification Preferences
-
- Choose how you want to be notified.
-
-
-
-
-
- Enable All Email Notifications
-
-
-
-
-
-
- New Meeting Bookings
-
-
-
-
-
- Meeting Confirmations/Cancellations
-
-
-
-
-
- Meeting Reminders
-
-
-
-
- Remind me before
-
-
-
-
-
- 15 minutes
- 30 minutes
- 1 hour
- 1 day
-
-
-
-
-
- Friend Requests
-
-
-
-
-
- Group Invitations/Updates
-
-
-
-
-
-
-
- Exit
- Save Changes
-
-
-
-
-
-
-
-
- Calendar & Availability
-
- Manage your calendar display, default availability, and iCal
- integrations.
-
-
-
-
-
- Display
-
-
-
- Default Calendar View
-
-
-
-
-
-
- Day
- Week
- Month
-
-
-
-
- Week Starts On
-
-
-
-
-
- Sunday
- Monday
-
-
-
-
-
- Show Weekends
-
-
-
-
-
-
-
- Availability
-
-
-
Working Hours
-
- Define your typical available hours (e.g.,
- Monday-Friday, 9 AM - 5 PM).
-
-
- Set Working Hours
-
-
-
-
- Minimum Notice for Bookings
-
-
- Min time before a booking can be made.
-
-
-
-
-
-
-
- Booking Window (days in advance)
-
-
- Max time in advance a booking can be made.
-
-
-
-
-
-
-
- iCalendar Integration
-
-
- Import iCal Feed URL
-
-
- Add Feed
-
-
-
- Export Your Calendar
-
- Get iCal Export URL
-
-
- Download .ics File
-
-
-
-
-
-
- Exit
- Save Changes
-
-
-
-
-
-
-
-
- Sharing & Privacy
-
- Control who can see your calendar and book time with you.
-
-
-
-
-
- Default Calendar Visibility
-
-
-
-
-
-
-
- Private (Only You)
-
-
- Free/Busy for Friends
-
-
- Full Details for Friends
-
-
-
-
-
-
- Who Can See Your Full Calendar Details?
-
-
- (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.
-
-
-
-
-
-
-
- Only Me
- My Friends
-
- Specific Friends/Groups (manage separately)
-
-
-
-
-
-
- Who Can Book Time With You?
-
-
-
-
-
-
- No One
- My Friends
-
- Specific Friends/Groups (manage separately)
-
-
-
-
-
-
Blocked Users
-
- Manage Blocked Users
-
-
- Prevent specific users from seeing your calendar or
- booking time.
-
-
-
-
-
- Exit
- Save Changes
-
-
-
-
-
-
-
-
- Appearance
-
- Customize the look and feel of the application.
-
-
-
-
- Theme
-
-
-
-
-
- Light
- Dark
- System Default
-
-
-
-
- Date Format
-
-
-
-
-
- DD/MM/YYYY
- MM/DD/YYYY
- YYYY-MM-DD
-
-
-
-
- Time Format
-
-
-
-
-
- 24-hour
- 12-hour
-
-
-
-
-
-
- Exit
- Save Changes
-
-
-
-
-
-
- );
+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 (
+
+
+
+
+
+
+
+ {currentSectionData?.label}
+
+ {currentSectionData?.description}
+
+
+
+
+
+
+
+
+
+
+ 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.
+
+
+
+
+ Display Name
+
+
+
+
Email Address
+
+
+ Email is managed by your SSO provider.
+
+
+
+
Profile Picture
+
+
+ Upload a new profile picture.
+
+
+
+ Timezone
+
+
+
+ Language
+
+
+
+
+
+ English
+ German
+
+
+
+
+
Delete Account
+
+ Permanently delete your account and all associated data.
+
+
+
+
+
+ );
+
+ case 'notifications':
+ return (
+
+
+
+ Notification Preferences
+
+ Choose how you want to be notified.
+
+
+
+
+
+ Enable All Email Notifications
+
+
+
+
+
+
+ New Meeting Bookings
+
+
+
+
+
+ Meeting Confirmations/Cancellations
+
+
+
+
+
+ Meeting Reminders
+
+
+
+
+ Remind me before
+
+
+
+
+
+ 15 minutes
+ 30 minutes
+ 1 hour
+ 1 day
+
+
+
+
+
+ Friend Requests
+
+
+
+
+
+ Group Invitations/Updates
+
+
+
+
+
+
+
+ );
+
+ case 'calendarAvailability':
+ return (
+
+
+
+ Calendar & Availability
+
+ Manage your calendar display, default availability, and iCal
+ integrations.
+
+
+
+
+ Display
+
+
+ Default Calendar View
+
+
+
+
+
+
+ Day
+ Week
+ Month
+
+
+
+
+ Week Starts On
+
+
+
+
+
+ Sunday
+ Monday
+
+
+
+
+
+ Show Weekends
+
+
+
+
+
+
+
+ Availability
+
+
+
Working Hours
+
+ Define your typical available hours (e.g., Monday-Friday,
+ 9 AM - 5 PM).
+
+
+ Set Working Hours
+
+
+
+
+ Minimum Notice for Bookings
+
+
+ Min time before a booking can be made.
+
+
+
+
+
+
+
+ Booking Window (days in advance)
+
+
+ Max time in advance a booking can be made.
+
+
+
+
+
+
+
+ iCalendar Integration
+
+
+ Import iCal Feed URL
+
+
+ Add Feed
+
+
+
+ Export Your Calendar
+
+ Get iCal Export URL
+
+
+ Download .ics File
+
+
+
+
+
+
+ );
+
+ case 'sharingPrivacy':
+ return (
+
+
+
+ Sharing & Privacy
+
+ Control who can see your calendar and book time with you.
+
+
+
+
+
+ Default Calendar Visibility
+
+
+
+
+
+
+
+ Private (Only You)
+
+
+ Free/Busy for Friends
+
+
+ Full Details for Friends
+
+
+
+
+
+
+ Who Can See Your Full Calendar Details?
+
+
+ (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.
+
+
+
+
+
+
+
+ Only Me
+ My Friends
+
+ Specific Friends/Groups (manage separately)
+
+
+
+
+
+
+ Who Can Book Time With You?
+
+
+
+
+
+
+ No One
+ My Friends
+
+ Specific Friends/Groups (manage separately)
+
+
+
+
+
+
Blocked Users
+
Manage Blocked Users
+
+ Prevent specific users from seeing your calendar or booking
+ time.
+
+
+
+
+
+ );
+
+ case 'appearance':
+ return (
+
+
+
+ Appearance
+
+ Customize the look and feel of the application.
+
+
+
+
+ Theme
+
+
+
+
+
+ Light
+ Dark
+ System Default
+
+
+
+
+ Date Format
+
+
+
+
+
+ DD/MM/YYYY
+ MM/DD/YYYY
+ YYYY-MM-DD
+
+
+
+
+ Time Format
+
+
+
+
+
+ 24-hour
+ 12-hour
+
+
+
+
+
+
+ );
+
+ default:
+ return null;
+ }
+ };
+
+ return (
+
+
+
+
+
{renderSettingsContent()}
+
+
+ router.back()} variant='secondary'>
+ Exit
+
+ Save Changes
+
+
+
+
+ );
+}
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 (
+
+
+ {title}
+
+
+
+
+
+ {selectedOption?.label || placeholder}
+ {selectedOption?.description && (
+
+ {selectedOption.description}
+
+ )}
+
+
+
+
+
+
+
+
+ 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.
-
-
-
- Display Name
-
-
-
-
Email Address
-
-
- Email is managed by your SSO provider.
-
-
+
+
+
+
+
+ Display Name
+
+
+
+
Email Address
+
+
+ Email might be managed by your SSO provider.
+
+
+
+
+
+
+
Profile Picture
@@ -67,7 +123,11 @@ export default function SettingsPage() {
Timezone
-
+
Language
@@ -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.
-
Theme
-
-
-
-
-
- Light
- Dark
- System Default
-
-
+
Date Format
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 (
-
-
- {title}
-
-
-
-
-
- {selectedOption?.label || placeholder}
- {selectedOption?.description && (
-
- {selectedOption.description}
-
- )}
-
-
-
-
-
-
-
-
- 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 (
);
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
-
+
- Display Name
-
+ >
-
-
Email Address
-
+
-
+ label='Email Address'
+ placeholder='Your E-Mail'
+ defaultValue={data?.data.user.email ?? ''}
+ >
+
+
Email might be managed by your SSO provider.
-
+
-
-
+
+
-
-
Profile Picture
-
-
- Upload a new profile picture.
-
+
+
+
+ {data?.data.user.image ? (
+
+ ) : (
+
+ )}
+
Timezone
@@ -143,9 +158,9 @@ export default function SettingsPage() {
Delete Account
-
+
Permanently delete your account and all associated data.
-
+
@@ -277,10 +292,10 @@ export default function SettingsPage() {
Working Hours
-
+
Define your typical available hours (e.g., Monday-Friday,
9 AM - 5 PM).
-
+
Set Working Hours
@@ -289,9 +304,9 @@ export default function SettingsPage() {
Minimum Notice for Bookings
-
+
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.
-
+
@@ -420,10 +435,10 @@ export default function SettingsPage() {
Blocked Users
Manage Blocked Users
-
+
Prevent specific users from seeing your calendar or booking
time.
-
+
diff --git a/src/components/wrappers/group-wrapper.tsx b/src/components/wrappers/group-wrapper.tsx
index 713b5d6..3006c9a 100644
--- a/src/components/wrappers/group-wrapper.tsx
+++ b/src/components/wrappers/group-wrapper.tsx
@@ -3,20 +3,20 @@ import type * as React from 'react';
interface ScrollableSettingsWrapperProps {
className?: string;
- legend?: string;
+ title?: string;
children: React.ReactNode;
}
export function GroupWrapper({
className,
- legend,
+ title,
children,
}: ScrollableSettingsWrapperProps) {
return (
- {legend}
+ {title}
{children}
);
From 8555fd919aeb663c410292fa1f607bb79f3e4427 Mon Sep 17 00:00:00 2001
From: Maximilian Liebmann
Date: Thu, 26 Jun 2025 22:48:56 +0200
Subject: [PATCH 8/8] feat: tempcommit
---
.../misc/profile-picture-upload.tsx | 35 ++++++
src/components/misc/settings-page.tsx | 110 +++++++++---------
2 files changed, 87 insertions(+), 58 deletions(-)
create mode 100644 src/components/misc/profile-picture-upload.tsx
diff --git a/src/components/misc/profile-picture-upload.tsx b/src/components/misc/profile-picture-upload.tsx
new file mode 100644
index 0000000..9ddf2a7
--- /dev/null
+++ b/src/components/misc/profile-picture-upload.tsx
@@ -0,0 +1,35 @@
+import Image from 'next/image';
+import { Avatar } from '../ui/avatar';
+import { useGetApiUserMe } from '@/generated/api/user/user';
+import { User } from 'lucide-react';
+
+import { Input } from '../ui/input';
+
+export default function ProfilePictureUpload() {
+ const { data } = useGetApiUserMe();
+ return (
+ <>
+
+ >
+ );
+}
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 ? (
-
- ) : (
-
- )}
-
-
-
- Timezone
-
-
-
- Language
-
-
-
-
-
- English
- German
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Language
+
+
+
+
+
+ English
+ German
+
+
+
+
+
+
+
Delete Account
Permanently delete your account and all associated data.