From 0cd88d6f07e58fffbb91a31a45e1d21e5a14e2d1 Mon Sep 17 00:00:00 2001 From: Maximilian Liebmann Date: Sun, 22 Jun 2025 23:22:53 +0200 Subject: [PATCH] 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 ( + + + + + + + ); +} diff --git a/src/components/custom-ui/app-sidebar.tsx b/src/components/custom-ui/app-sidebar.tsx index ae00c9b..4861363 100644 --- a/src/components/custom-ui/app-sidebar.tsx +++ b/src/components/custom-ui/app-sidebar.tsx @@ -6,27 +6,27 @@ import { SidebarContent, SidebarFooter, SidebarGroup, - SidebarGroupAction, + // SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, - SidebarInput, - SidebarInset, + // SidebarInput, + // SidebarInset, SidebarMenu, - SidebarMenuAction, - SidebarMenuBadge, + // SidebarMenuAction, + // SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, - SidebarMenuSkeleton, - SidebarMenuSub, - SidebarMenuSubButton, - SidebarMenuSubItem, - SidebarProvider, - SidebarRail, - SidebarSeparator, - SidebarTrigger, - useSidebar, -} from '@/components/ui/sidebar'; + // SidebarMenuSkeleton, + // SidebarMenuSub, + // SidebarMenuSubButton, + // SidebarMenuSubItem, + // SidebarProvider, + // SidebarRail, + // SidebarSeparator, + // SidebarTrigger, + // useSidebar, +} from '@/components/custom-ui/sidebar'; import { ChevronDown } from 'lucide-react'; import { @@ -39,8 +39,6 @@ import Logo from '@/components/misc/logo'; import Link from 'next/link'; -import { ThemePicker } from '@/components/misc/theme-picker'; - import { Star, CalendarDays, diff --git a/src/components/ui/sidebar.tsx b/src/components/custom-ui/sidebar.tsx similarity index 99% rename from src/components/ui/sidebar.tsx rename to src/components/custom-ui/sidebar.tsx index 6b68b8f..11228cb 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/components/custom-ui/sidebar.tsx @@ -466,7 +466,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) {
  • ); diff --git a/src/components/misc/header.tsx b/src/components/misc/header.tsx index dbf8a1f..ed53953 100644 --- a/src/components/misc/header.tsx +++ b/src/components/misc/header.tsx @@ -1,5 +1,22 @@ -import { SidebarTrigger } from '@/components/ui/sidebar'; +import { SidebarTrigger } from '@/components/custom-ui/sidebar'; import { ThemePicker } from '@/components/misc/theme-picker'; +import { NotificationButton } from '@/components/buttons/notification-button'; + +import { BellRing, Inbox } from 'lucide-react'; +import UserDropdown from '@/components/misc/user-dropdown'; + +const items = [ + { + title: 'Calendar', + url: '#', + icon: Inbox, + }, + { + title: 'Friends', + url: '#', + icon: BellRing, + }, +]; export default function Header({ children, @@ -8,13 +25,24 @@ export default function Header({ }>) { return (
    -
    +
    Search - + + {items.map((item) => ( + + + + ))} +
    {children}
    diff --git a/src/components/misc/nav-user.tsx b/src/components/misc/nav-user.tsx new file mode 100644 index 0000000..53ab582 --- /dev/null +++ b/src/components/misc/nav-user.tsx @@ -0,0 +1,110 @@ +'use client'; + +import { + BadgeCheck, + Bell, + ChevronsUpDown, + CreditCard, + LogOut, + Sparkles, +} from 'lucide-react'; + +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from '@/components/custom-ui/sidebar'; + +export function NavUser({ + user, +}: { + user: { + name: string; + email: string; + avatar: string; + }; +}) { + const { isMobile } = useSidebar(); + + return ( + + + + + + + + CN + +
    + {user.name} + {user.email} +
    + +
    +
    + + +
    + + + CN + +
    + {user.name} + {user.email} +
    +
    +
    + + + + + Upgrade to Pro + + + + + + + Account + + + + Billing + + + + Notifications + + + + + + Log out + +
    +
    +
    +
    + ); +} diff --git a/src/components/misc/notification-dot.tsx b/src/components/misc/notification-dot.tsx new file mode 100644 index 0000000..a918188 --- /dev/null +++ b/src/components/misc/notification-dot.tsx @@ -0,0 +1,35 @@ +import { cn } from '@/lib/utils'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { CircleSmall } from 'lucide-react'; + +const dotVariants = cva('', { + variants: { + variant: { + neutral: 'fill-neutral-900', + active: 'fill-red-600 stroke-red-600', + hidden: 'hidden', + }, + }, + defaultVariants: { + variant: 'hidden', + }, +}); + +function NotificationDot({ + className, + dotVariant, + ...props +}: { + className: string; + dotVariant: VariantProps['variant']; +}) { + return ( + + ); +} + +export type NDot = VariantProps['variant']; +export { NotificationDot, dotVariants }; diff --git a/src/components/misc/user-card.tsx b/src/components/misc/user-card.tsx new file mode 100644 index 0000000..faefc35 --- /dev/null +++ b/src/components/misc/user-card.tsx @@ -0,0 +1,29 @@ +import { useGetApiUserMe } from '@/generated/api/user/user'; +import { Avatar } from '@/components/ui/avatar'; +import Image from 'next/image'; +import { User } from 'lucide-react'; + +export default function UserCard() { + const { data } = useGetApiUserMe(); + return ( +
    + + {data?.data.user.image ? ( + Avatar + ) : ( + + )} + +
    {data?.data.user.name}
    +
    + {data?.data.user.email} +
    +
    + ); +} diff --git a/src/components/misc/user-dropdown.tsx b/src/components/misc/user-dropdown.tsx new file mode 100644 index 0000000..8f5aa05 --- /dev/null +++ b/src/components/misc/user-dropdown.tsx @@ -0,0 +1,59 @@ +'use client'; + +import { Avatar } from '@/components/ui/avatar'; +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + // DropdownMenuLabel, + // DropdownMenuPortal, + DropdownMenuSeparator, + // DropdownMenuShortcut, + // DropdownMenuSub, + // DropdownMenuSubContent, + // DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { useGetApiUserMe } from '@/generated/api/user/user'; +import { ChevronDown, User } from 'lucide-react'; +import Image from 'next/image'; +import Link from 'next/link'; +import UserCard from '@/components/misc/user-card'; + +export default function UserDropdown() { + const { data } = useGetApiUserMe(); + return ( + + + + + + + + + + Settings + + + Logout + + + + ); +} diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx new file mode 100644 index 0000000..6a21b65 --- /dev/null +++ b/src/components/ui/avatar.tsx @@ -0,0 +1,53 @@ +'use client'; + +import * as React from 'react'; +import * as AvatarPrimitive from '@radix-ui/react-avatar'; + +import { cn } from '@/lib/utils'; + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx index 84649ad..e8d5ec1 100644 --- a/src/components/ui/sheet.tsx +++ b/src/components/ui/sheet.tsx @@ -1,31 +1,31 @@ -"use client" +'use client'; -import * as React from "react" -import * as SheetPrimitive from "@radix-ui/react-dialog" -import { XIcon } from "lucide-react" +import * as React from 'react'; +import * as SheetPrimitive from '@radix-ui/react-dialog'; +import { XIcon } from 'lucide-react'; -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils'; function Sheet({ ...props }: React.ComponentProps) { - return + return ; } function SheetTrigger({ ...props }: React.ComponentProps) { - return + return ; } function SheetClose({ ...props }: React.ComponentProps) { - return + return ; } function SheetPortal({ ...props }: React.ComponentProps) { - return + return ; } function SheetOverlay({ @@ -34,71 +34,71 @@ function SheetOverlay({ }: React.ComponentProps) { return ( - ) + ); } function SheetContent({ className, children, - side = "right", + side = 'right', ...props }: React.ComponentProps & { - side?: "top" | "right" | "bottom" | "left" + side?: 'top' | 'right' | 'bottom' | 'left'; }) { return ( {children} - - - Close + + + Close - ) + ); } -function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { +function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) { return (
    - ) + ); } -function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { +function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) { return (
    - ) + ); } function SheetTitle({ @@ -107,11 +107,11 @@ function SheetTitle({ }: React.ComponentProps) { return ( - ) + ); } function SheetDescription({ @@ -120,11 +120,11 @@ function SheetDescription({ }: React.ComponentProps) { return ( - ) + ); } export { @@ -136,4 +136,4 @@ export { SheetFooter, SheetTitle, SheetDescription, -} +}; diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx index 32ea0ef..a9344b2 100644 --- a/src/components/ui/skeleton.tsx +++ b/src/components/ui/skeleton.tsx @@ -1,13 +1,13 @@ -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils'; -function Skeleton({ className, ...props }: React.ComponentProps<"div">) { +function Skeleton({ className, ...props }: React.ComponentProps<'div'>) { return (
    - ) + ); } -export { Skeleton } +export { Skeleton }; diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx index 4ee26b3..2b8b1d7 100644 --- a/src/components/ui/tooltip.tsx +++ b/src/components/ui/tooltip.tsx @@ -1,9 +1,9 @@ -"use client" +'use client'; -import * as React from "react" -import * as TooltipPrimitive from "@radix-ui/react-tooltip" +import * as React from 'react'; +import * as TooltipPrimitive from '@radix-ui/react-tooltip'; -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils'; function TooltipProvider({ delayDuration = 0, @@ -11,11 +11,11 @@ function TooltipProvider({ }: React.ComponentProps) { return ( - ) + ); } function Tooltip({ @@ -23,15 +23,15 @@ function Tooltip({ }: React.ComponentProps) { return ( - + - ) + ); } function TooltipTrigger({ ...props }: React.ComponentProps) { - return + return ; } function TooltipContent({ @@ -43,19 +43,19 @@ function TooltipContent({ return ( {children} - + - ) + ); } -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/src/components/query-provider.tsx b/src/components/wrappers/query-provider.tsx similarity index 100% rename from src/components/query-provider.tsx rename to src/components/wrappers/query-provider.tsx diff --git a/src/components/wrappers/sidebar-provider.tsx b/src/components/wrappers/sidebar-provider.tsx index 3873fa4..3c9ff95 100644 --- a/src/components/wrappers/sidebar-provider.tsx +++ b/src/components/wrappers/sidebar-provider.tsx @@ -1,7 +1,7 @@ 'use client'; import React from 'react'; -import { SidebarProvider } from '../ui/sidebar'; +import { SidebarProvider } from '../custom-ui/sidebar'; export default function SidebarProviderWrapper({ defaultOpen, diff --git a/src/hooks/use-mobile.ts b/src/hooks/use-mobile.ts index 2b0fe1d..821f8ff 100644 --- a/src/hooks/use-mobile.ts +++ b/src/hooks/use-mobile.ts @@ -1,19 +1,21 @@ -import * as React from "react" +import * as React from 'react'; -const MOBILE_BREAKPOINT = 768 +const MOBILE_BREAKPOINT = 768; export function useIsMobile() { - const [isMobile, setIsMobile] = React.useState(undefined) + const [isMobile, setIsMobile] = React.useState( + undefined, + ); React.useEffect(() => { - const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); const onChange = () => { - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) - } - mql.addEventListener("change", onChange) - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) - return () => mql.removeEventListener("change", onChange) - }, []) + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + }; + mql.addEventListener('change', onChange); + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + return () => mql.removeEventListener('change', onChange); + }, []); - return !!isMobile + return !!isMobile; } diff --git a/yarn.lock b/yarn.lock index e1ff28c..90dc258 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1299,6 +1299,29 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-avatar@npm:^1.1.10": + version: 1.1.10 + resolution: "@radix-ui/react-avatar@npm:1.1.10" + dependencies: + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-callback-ref": "npm:1.1.1" + "@radix-ui/react-use-is-hydrated": "npm:0.1.0" + "@radix-ui/react-use-layout-effect": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/9fb0cf9a9d0fdbeaa2efda476402fc09db2e6ff9cd9aa3ea1d315d9c9579840722a4833725cb196c455e0bd775dfe04221a4f6855685ce89d2133c42e2b07e5f + languageName: node + linkType: hard + "@radix-ui/react-collapsible@npm:^1.1.11": version: 1.1.11 resolution: "@radix-ui/react-collapsible@npm:1.1.11" @@ -1441,7 +1464,7 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-dropdown-menu@npm:^2.1.14": +"@radix-ui/react-dropdown-menu@npm:^2.1.15": version: 2.1.15 resolution: "@radix-ui/react-dropdown-menu@npm:2.1.15" dependencies: @@ -1951,6 +1974,21 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-use-is-hydrated@npm:0.1.0": + version: 0.1.0 + resolution: "@radix-ui/react-use-is-hydrated@npm:0.1.0" + dependencies: + use-sync-external-store: "npm:^1.5.0" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/635079bafe32829fc7405895154568ea94a22689b170489fd6d77668e4885e72ff71ed6d0ea3d602852841ef0f1927aa400fee2178d5dfbeb8bc9297da7d6498 + languageName: node + linkType: hard + "@radix-ui/react-use-layout-effect@npm:1.1.1": version: 1.1.1 resolution: "@radix-ui/react-use-layout-effect@npm:1.1.1" @@ -6637,9 +6675,10 @@ __metadata: "@fortawesome/react-fontawesome": "npm:^0.2.2" "@hookform/resolvers": "npm:^5.0.1" "@prisma/client": "npm:^6.9.0" + "@radix-ui/react-avatar": "npm:^1.1.10" "@radix-ui/react-collapsible": "npm:^1.1.11" "@radix-ui/react-dialog": "npm:^1.1.14" - "@radix-ui/react-dropdown-menu": "npm:^2.1.14" + "@radix-ui/react-dropdown-menu": "npm:^2.1.15" "@radix-ui/react-hover-card": "npm:^1.1.13" "@radix-ui/react-label": "npm:^2.1.6" "@radix-ui/react-scroll-area": "npm:^1.2.8" @@ -9385,7 +9424,7 @@ __metadata: languageName: node linkType: hard -"use-sync-external-store@npm:^1.4.0": +"use-sync-external-store@npm:^1.4.0, use-sync-external-store@npm:^1.5.0": version: 1.5.0 resolution: "use-sync-external-store@npm:1.5.0" peerDependencies: