-
Notifications
You must be signed in to change notification settings - Fork 1
feat: reorganize navigation menu for mobile view #298
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8b7c24d
9132214
5a44e7c
25ddcd2
77c94fa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,161 @@ | ||||||||||||||||||||||||||||||||||||||||
'use client' | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet' | ||||||||||||||||||||||||||||||||||||||||
import { Button } from '@/components/ui/button' | ||||||||||||||||||||||||||||||||||||||||
import { LogOut } from 'lucide-react' | ||||||||||||||||||||||||||||||||||||||||
import { signOut } from 'next-auth/react' | ||||||||||||||||||||||||||||||||||||||||
import Image from 'next/image' | ||||||||||||||||||||||||||||||||||||||||
import { appConfig } from 'mb-env' | ||||||||||||||||||||||||||||||||||||||||
import type { Session } from 'next-auth' | ||||||||||||||||||||||||||||||||||||||||
import { useState, useCallback } from 'react' | ||||||||||||||||||||||||||||||||||||||||
import { useRouter } from 'next/navigation' | ||||||||||||||||||||||||||||||||||||||||
import { toSlug } from 'mb-lib' | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
interface ProfileSidebarProps { | ||||||||||||||||||||||||||||||||||||||||
user: Session['user'] & { | ||||||||||||||||||||||||||||||||||||||||
hasuraJwt?: string | ||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
function getUserInitials(name: string) { | ||||||||||||||||||||||||||||||||||||||||
const [firstName, lastName] = name.split(' ') | ||||||||||||||||||||||||||||||||||||||||
return lastName ? `${firstName[0]}${lastName[0]}` : firstName.slice(0, 2) | ||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||
Comment on lines
+20
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add input validation and handle edge cases in getUserInitials. The current implementation could throw errors for edge cases. Consider these additional improvements: -function getUserInitials(name: string) {
+function getUserInitials(name: string = ''): string {
+ if (!name?.trim()) return '';
const [firstName, lastName] = name.split(' ')
- return lastName ? `${firstName[0]}${lastName[0]}` : firstName.slice(0, 2)
+ return lastName && firstName?.[0] && lastName?.[0]
+ ? `${firstName[0]}${lastName[0]}`
+ : (firstName?.slice(0, 2) || '')
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
export function ProfileSidebar({ user }: ProfileSidebarProps) { | ||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (complexity): Consider refactoring the component to reduce duplication and improve code organization through component extraction and data structures The component has some unnecessary complexity that can be simplified while maintaining functionality:
function UserAvatar({ user, size = 'md' }: { user: User, size?: 'sm' | 'md' }) {
const sizes = {
sm: 'size-8',
md: 'size-10'
}
return user?.image ? (
<Image
className={`rounded-full ${sizes[size]}`}
src={user.image}
alt={user.name ?? 'Avatar'}
height={size === 'sm' ? 32 : 40}
width={size === 'sm' ? 32 : 40}
priority
/>
) : (
<div className={`flex items-center justify-center text-sm font-medium uppercase rounded-full ${sizes[size]} bg-muted/50`}>
{user?.name ? getUserInitials(user?.name) : null}
</div>
)
}
const handleLogout = useCallback(async () => {
try {
setIsOpen(false)
await signOut({ callbackUrl: '/' })
} catch (error) {
console.error('Logout error:', error)
window.location.href = '/'
}
}, [])
const navLinks = [
{ label: 'Chat', path: '/c' },
...(appConfig.devMode ? [
{ label: 'Pro', path: '/c/p' },
{ label: 'Ww', path: '/wordware' }
] : []),
{ label: 'Browse', path: '/' }
]
// In JSX:
<nav className="flex flex-col p-4 lg:hidden">
{navLinks.map(({ label, path }) => (
<Button
key={path}
variant="ghost"
className="w-full justify-start text-sm"
onClick={() => handleNavigation(path)}
>
{label}
</Button>
))}
</nav> These changes reduce code duplication and improve maintainability while keeping all functionality intact. |
||||||||||||||||||||||||||||||||||||||||
const [isOpen, setIsOpen] = useState(false) | ||||||||||||||||||||||||||||||||||||||||
const router = useRouter() | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
const handleNavigation = (path: string) => { | ||||||||||||||||||||||||||||||||||||||||
setIsOpen(false) | ||||||||||||||||||||||||||||||||||||||||
router.push(path) | ||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
const handleLogout = useCallback(async () => { | ||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||
setIsOpen(false) | ||||||||||||||||||||||||||||||||||||||||
await new Promise(resolve => setTimeout(resolve, 100)) | ||||||||||||||||||||||||||||||||||||||||
await signOut({ callbackUrl: '/' }) | ||||||||||||||||||||||||||||||||||||||||
} catch (error) { | ||||||||||||||||||||||||||||||||||||||||
console.error('Logout error:', error) | ||||||||||||||||||||||||||||||||||||||||
window.location.href = '/' | ||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||
}, []) | ||||||||||||||||||||||||||||||||||||||||
Comment on lines
+34
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Improve logout handler implementation. The current implementation has several concerns:
const handleLogout = useCallback(async () => {
try {
setIsOpen(false)
- await new Promise(resolve => setTimeout(resolve, 100))
await signOut({ callbackUrl: '/' })
} catch (error) {
- console.error('Logout error:', error)
+ console.error('Failed to sign out:', error instanceof Error ? error.message : 'Unknown error')
window.location.href = '/'
}
}, []) 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
const goToProfile = useCallback((e: React.MouseEvent) => { | ||||||||||||||||||||||||||||||||||||||||
e.preventDefault() | ||||||||||||||||||||||||||||||||||||||||
e.stopPropagation() | ||||||||||||||||||||||||||||||||||||||||
const userSlug = toSlug(user.name || '') | ||||||||||||||||||||||||||||||||||||||||
if (userSlug) { | ||||||||||||||||||||||||||||||||||||||||
setIsOpen(false) | ||||||||||||||||||||||||||||||||||||||||
router.push(`/u/${userSlug}/t`) | ||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||
}, [router, user.name]) | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||||||||||||||||||
<Sheet open={isOpen} onOpenChange={setIsOpen}> | ||||||||||||||||||||||||||||||||||||||||
<SheetTrigger asChild> | ||||||||||||||||||||||||||||||||||||||||
<Button variant="ghost" className="pl-0 rounded-full"> | ||||||||||||||||||||||||||||||||||||||||
{user?.image ? ( | ||||||||||||||||||||||||||||||||||||||||
<Image | ||||||||||||||||||||||||||||||||||||||||
className="transition-opacity duration-300 rounded-full select-none size-8 bg-foreground/10 ring-1 ring-zinc-100/10 hover:opacity-80" | ||||||||||||||||||||||||||||||||||||||||
src={user?.image ? user.image : ''} | ||||||||||||||||||||||||||||||||||||||||
alt={user.name ?? 'Avatar'} | ||||||||||||||||||||||||||||||||||||||||
height={32} | ||||||||||||||||||||||||||||||||||||||||
width={32} | ||||||||||||||||||||||||||||||||||||||||
priority | ||||||||||||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||||||||||||
) : ( | ||||||||||||||||||||||||||||||||||||||||
<div className="flex items-center justify-center text-xs font-medium uppercase rounded-full select-none size-7 shrink-0 bg-muted/50 text-muted-foreground"> | ||||||||||||||||||||||||||||||||||||||||
{user?.name ? getUserInitials(user?.name) : null} | ||||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||||
)} | ||||||||||||||||||||||||||||||||||||||||
<span className="ml-2 hidden md:inline-block">{user?.name}</span> | ||||||||||||||||||||||||||||||||||||||||
</Button> | ||||||||||||||||||||||||||||||||||||||||
</SheetTrigger> | ||||||||||||||||||||||||||||||||||||||||
<SheetContent side="right" className="w-[300px] sm:w-[400px] p-0"> | ||||||||||||||||||||||||||||||||||||||||
<div className="flex flex-col h-full"> | ||||||||||||||||||||||||||||||||||||||||
{/* Profile Header */} | ||||||||||||||||||||||||||||||||||||||||
<div className="p-4 border-b"> | ||||||||||||||||||||||||||||||||||||||||
<Button | ||||||||||||||||||||||||||||||||||||||||
onClick={goToProfile} | ||||||||||||||||||||||||||||||||||||||||
variant="sideBarProfile" | ||||||||||||||||||||||||||||||||||||||||
size="sideBarProfile" | ||||||||||||||||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||||||||||||||||
{user?.image ? ( | ||||||||||||||||||||||||||||||||||||||||
<Image | ||||||||||||||||||||||||||||||||||||||||
className="rounded-full size-10" | ||||||||||||||||||||||||||||||||||||||||
src={user.image} | ||||||||||||||||||||||||||||||||||||||||
alt={user.name ?? 'Avatar'} | ||||||||||||||||||||||||||||||||||||||||
height={40} | ||||||||||||||||||||||||||||||||||||||||
width={40} | ||||||||||||||||||||||||||||||||||||||||
priority | ||||||||||||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||||||||||||
) : ( | ||||||||||||||||||||||||||||||||||||||||
<div className="flex items-center justify-center text-sm font-medium uppercase rounded-full size-10 bg-muted/50"> | ||||||||||||||||||||||||||||||||||||||||
{user?.name ? getUserInitials(user?.name) : null} | ||||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||||
)} | ||||||||||||||||||||||||||||||||||||||||
<div className="space-y-1"> | ||||||||||||||||||||||||||||||||||||||||
<p className="text-sm font-medium">{user?.name}</p> | ||||||||||||||||||||||||||||||||||||||||
<p className="text-xs text-muted-foreground">{user?.email}</p> | ||||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||||
</Button> | ||||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
{/* Navigation Links - Only visible on mobile */} | ||||||||||||||||||||||||||||||||||||||||
<nav className="flex flex-col p-4 lg:hidden"> | ||||||||||||||||||||||||||||||||||||||||
<Button | ||||||||||||||||||||||||||||||||||||||||
variant="ghost" | ||||||||||||||||||||||||||||||||||||||||
className="w-full justify-start text-sm" | ||||||||||||||||||||||||||||||||||||||||
onClick={() => handleNavigation('/c')} | ||||||||||||||||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||||||||||||||||
Chat | ||||||||||||||||||||||||||||||||||||||||
</Button> | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
{appConfig.devMode && ( | ||||||||||||||||||||||||||||||||||||||||
<Button | ||||||||||||||||||||||||||||||||||||||||
variant="ghost" | ||||||||||||||||||||||||||||||||||||||||
className="w-full justify-start text-sm" | ||||||||||||||||||||||||||||||||||||||||
onClick={() => handleNavigation('/c/p')} | ||||||||||||||||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||||||||||||||||
Pro | ||||||||||||||||||||||||||||||||||||||||
</Button> | ||||||||||||||||||||||||||||||||||||||||
)} | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
<Button | ||||||||||||||||||||||||||||||||||||||||
variant="ghost" | ||||||||||||||||||||||||||||||||||||||||
className="w-full justify-start text-sm" | ||||||||||||||||||||||||||||||||||||||||
onClick={() => handleNavigation('/')} | ||||||||||||||||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||||||||||||||||
Browse | ||||||||||||||||||||||||||||||||||||||||
</Button> | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
{appConfig.devMode && ( | ||||||||||||||||||||||||||||||||||||||||
<Button | ||||||||||||||||||||||||||||||||||||||||
variant="ghost" | ||||||||||||||||||||||||||||||||||||||||
className="w-full justify-start text-sm" | ||||||||||||||||||||||||||||||||||||||||
onClick={() => handleNavigation('/wordware')} | ||||||||||||||||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||||||||||||||||
Ww | ||||||||||||||||||||||||||||||||||||||||
</Button> | ||||||||||||||||||||||||||||||||||||||||
)} | ||||||||||||||||||||||||||||||||||||||||
</nav> | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
{/* Logout Button */} | ||||||||||||||||||||||||||||||||||||||||
<div className="mt-auto p-4 border-t"> | ||||||||||||||||||||||||||||||||||||||||
<Button | ||||||||||||||||||||||||||||||||||||||||
variant="ghost" | ||||||||||||||||||||||||||||||||||||||||
className="w-full justify-start text-sm" | ||||||||||||||||||||||||||||||||||||||||
onClick={handleLogout} | ||||||||||||||||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||||||||||||||||
<LogOut className="size-4 mr-2" /> | ||||||||||||||||||||||||||||||||||||||||
Log Out | ||||||||||||||||||||||||||||||||||||||||
</Button> | ||||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||||
</SheetContent> | ||||||||||||||||||||||||||||||||||||||||
</Sheet> | ||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: Add input validation to getUserInitials to handle edge cases
The function should handle empty strings, strings with only spaces, or single-word names without throwing errors.