Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/layouts/MainLayout/BaseLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Tooltip } from '@/shared/Tooltip'
import ThemeToggleSwitch from '@/ThemeToggleSwitch'
import AvatarMenu from '@/shared/composites/AvatarMenu/AvatarMenu'
import { Tooltip } from '@/shared/primitives/Tooltip'
// import ThemeToggleSwitch from '@/ThemeToggleSwitch'
import React, { Suspense } from 'react'
import { Outlet } from 'react-router-dom'

Expand All @@ -12,7 +13,11 @@ const BaseLayout: React.FC = () => {
<Suspense fallback={null}>
<Outlet />
<div className="absolute top-4 right-10 flex gap-2">
<Tooltip children={<ThemeToggleSwitch />} content="Change theme" position="left" />
<Tooltip
children={<AvatarMenu name="Admin Mod" />}
content="Settings"
position="left"
/>
</div>
</Suspense>
</div>
Expand Down
2 changes: 0 additions & 2 deletions src/shared/Button/index.ts

This file was deleted.

41 changes: 41 additions & 0 deletions src/shared/composites/AvatarMenu/AvatarMenu.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { Meta, StoryObj } from '@storybook/react-vite'

import { fn } from 'storybook/test'

import { AvatarMenu } from './AvatarMenu'

const meta = {
title: 'Shared/Composites/AvatarMenu',

component: AvatarMenu,

tags: ['autodocs'],

args: {
onEditProfile: fn(),

onPreferences: fn(),

onSecurity: fn(),

onLogout: fn(),
},
} satisfies Meta<typeof AvatarMenu>

export default meta

type Story = StoryObj<typeof meta>

export const Default: Story = {
args: {
name: 'Admin Mod',
},
}

export const WithImage: Story = {
args: {
name: 'Admin Mod',

src: 'https://i.pravatar.cc/150?img=12',
},
}
153 changes: 153 additions & 0 deletions src/shared/composites/AvatarMenu/AvatarMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { useEffect, useRef, useState } from 'react'

import { Avatar } from '@/shared/primitives/Avatar'
import { Switch } from '@/shared/primitives/Switch/Switch'
import {
ArrowRightStartOnRectangleIcon,
PencilSquareIcon,
// Cog6ToothIcon,
MoonIcon,
SunIcon,
AdjustmentsHorizontalIcon,
LockClosedIcon,
// UserCircleIcon,
// ShieldCheckIcon,
} from '@heroicons/react/24/outline'
import { useTheme } from '@/shared/theme'
import { Button } from '@/shared/primitives/Button'
export interface AvatarMenuProps {
name: string

src?: string

onEditProfile?: () => void

onPreferences?: () => void

onSecurity?: () => void

onLogout?: () => void
}

export function AvatarMenu({
name,
src,

onEditProfile = () => {},
onPreferences = () => {},
onSecurity = () => {},
onLogout = () => {},
}: AvatarMenuProps) {
const [open, setOpen] = useState(false)
const { themeName, setTheme } = useTheme()
const ref = useRef<HTMLDivElement>(null)

useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false)
}
}

document.addEventListener('mousedown', handleClickOutside)

return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [])

return (
<div ref={ref} className="relative inline-block">
{/* Trigger */}
<Button
onClick={() => setOpen((prev) => !prev)}
className="m-0 cursor-pointer rounded-lg p-0 focus:outline-none"
size="sd"
variant={'ghost'}
>
<Avatar name={name} src={src} className={'rounded-md'} />
</Button>

{/* Dropdown */}
{open && (
<div className="border-border-primary bg-bg-secondary absolute right-0 z-50 mt-2 w-56 rounded-lg border p-1 shadow-md">
{/* Header */}
<div className="border-border-tertiary border-b px-3 py-2">
<div className="text-text-primary text-sm font-medium">{name}</div>

<div className="text-text-secondary text-xs">Moderator account</div>
</div>

{/* Items */}
<Button
onClick={onEditProfile}
className="text-text-primary hover:bg-bg-tertiary w-full cursor-pointer rounded-md px-3 py-2 text-left text-sm transition"
variant={'ghost'}
leftIcon={<PencilSquareIcon className="h-5 w-5" />}
>
Edit profile
</Button>
<Button
onClick={onSecurity}
className="text-text-primary hover:bg-bg-tertiary w-full cursor-pointer rounded-md px-3 py-2 text-left text-sm transition"
variant={'ghost'}
leftIcon={<LockClosedIcon className="h-5 w-5" />}
>
Security settings
</Button>

<div className="bg-border-tertiary my-1 h-px" />
<Button
onClick={onPreferences}
className="text-text-primary hover:bg-bg-tertiary w-full cursor-pointer rounded-md px-3 py-2 text-left text-sm transition"
textAlign={'left'}
variant={'ghost'}
leftIcon={<AdjustmentsHorizontalIcon className="h-5 w-5" />}
>
Preferences
</Button>
<Button
className="flex items-center justify-between px-3 py-2"
variant={'ghost'}
rightIcon={
<Switch
className="ms-6"
checked={themeName === 'dark'}
onChange={(checked: boolean) => {
return setTheme(checked ? 'dark' : 'light')
}}
/>
}
>
<div className="text-text-primary flex items-center gap-3 text-sm">
{themeName === 'dark' ? (
<SunIcon className="size-4" />
) : (
<MoonIcon className="size-4" />
)}

{themeName === 'dark' ? (
<div className="text-text-secondary text-sm">Light mode</div>
) : (
<div className="text-text-secondary text-sm">Dark mode</div>
)}
</div>
</Button>
<div className="bg-border-tertiary my-1 h-px" />

<Button
onClick={onLogout}
className="contents-center text-text-danger hover:bg-bg-danger flex w-full cursor-pointer flex-row items-center gap-2 rounded-md px-3 py-2 text-left text-sm transition"
variant={'danger'}
leftIcon={<ArrowRightStartOnRectangleIcon className="h-5 w-5" />}
>
{/* <div><ArrowRightStartOnRectangleIcon className='w-5 h-5' /></div> */}
<div>Logout</div>
</Button>
</div>
)}
</div>
)
}

export default AvatarMenu
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import { cva } from 'class-variance-authority'
import { Badge } from '../Badge'
import { Badge } from '../../primitives/Badge'
// import { NavLink } from 'react-router-dom'

const sidebarNavItem = cva(
Expand Down
77 changes: 77 additions & 0 deletions src/shared/primitives/Avatar/Avatar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { Meta, StoryObj } from '@storybook/react-vite'

import { Avatar } from './Avatar'

const meta = {
title: 'Shared/Primitives/Avatar',

component: Avatar,

tags: ['autodocs'],
} satisfies Meta<typeof Avatar>

export default meta

type Story = StoryObj<typeof meta>

export const Image: Story = {
args: {
name: 'Admin Mod',

src: 'https://i.pravatar.cc/150?img=12',
},
}

export const InitialsFallback: Story = {
args: {
name: 'Admin Mod',
},
}

export const Online: Story = {
args: {
name: 'Admin Mod',

showOnline: true,
},
}

export const ExtraSmall: Story = {
args: {
name: 'Admin Mod',

size: 'xs',
},
}

export const Small: Story = {
args: {
name: 'Admin Mod',

size: 'sm',
},
}

export const Medium: Story = {
args: {
name: 'Admin Mod',

size: 'md',
},
}

export const Large: Story = {
args: {
name: 'Admin Mod',

size: 'lg',
},
}

export const BrokenImageFallback: Story = {
args: {
name: 'Admin Mod',

src: '/broken-image.png',
},
}
Loading
Loading