start creating dashboard

This commit is contained in:
IluaAir
2025-10-10 23:58:57 +03:00
parent 31e8e355f3
commit 2c35b781dd
6 changed files with 363 additions and 0 deletions

View File

@@ -3,6 +3,7 @@ import { LoginPage } from './pages/Login'
import { SignUp } from './pages/SignUp'
import { AuthLayout } from './layouts/AuthLayout'
import { Routes, Route, Navigate } from 'react-router'
import MenuDockVertical from './layouts/DashLayout'
function App() {
@@ -18,6 +19,11 @@ function App() {
<Route path="signup" element={<SignUp />} />
</Route>
<Route path="/dashboard" element={
// <div className="min-h-svh bg-background flex justify-start items-start">
<MenuDockVertical />
// </div>
} />
<Route path="*" element={<Navigate to="/auth/login" replace />} />
</Routes>
)

View File

@@ -0,0 +1,45 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
function Avatar({
className,
...props
}) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn("relative flex size-8 shrink-0 overflow-hidden rounded-full", className)}
{...props} />
);
}
function AvatarImage({
className,
...props
}) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props} />
);
}
function AvatarFallback({
className,
...props
}) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className
)}
{...props} />
);
}
export { Avatar, AvatarImage, AvatarFallback }

View File

@@ -0,0 +1,185 @@
'use client';;
import React, { useState, useRef, useEffect, useMemo } from 'react';
import { Home, Briefcase, Calendar, Shield, Settings } from 'lucide-react';
import { cn } from '@/lib/utils';
const defaultItems = [
{ label: 'home', icon: Home },
{ label: 'work', icon: Briefcase },
{ label: 'calendar', icon: Calendar },
{ label: 'security', icon: Shield },
{ label: 'settings', icon: Settings },
];
export const MenuDock = ({
items,
className,
variant = 'default',
orientation = 'horizontal',
showLabels = true,
animated = true
}) => {
const finalItems = useMemo(() => {
const isValid = items && Array.isArray(items) && items.length >= 2 && items.length <= 8;
if (!isValid) {
console.warn(
"MenuDock: 'items' prop is invalid or missing. Using default items.",
items
);
return defaultItems;
}
return items;
}, [items]);
const [activeIndex, setActiveIndex] = useState(0);
const [underlineWidth, setUnderlineWidth] = useState(0);
const [underlineLeft, setUnderlineLeft] = useState(0);
const textRefs = useRef([]);
const itemRefs = useRef([]);
useEffect(() => {
if (activeIndex >= finalItems.length) {
setActiveIndex(0);
}
}, [finalItems, activeIndex]);
useEffect(() => {
const updateUnderline = () => {
const activeButton = itemRefs.current[activeIndex];
const activeText = textRefs.current[activeIndex];
if (activeButton && activeText && showLabels && orientation === 'horizontal') {
const buttonRect = activeButton.getBoundingClientRect();
const textRect = activeText.getBoundingClientRect();
const containerRect = activeButton.parentElement?.getBoundingClientRect();
if (containerRect) {
setUnderlineWidth(textRect.width);
setUnderlineLeft(
buttonRect.left - containerRect.left + (buttonRect.width - textRect.width) / 2
);
}
}
};
updateUnderline();
window.addEventListener('resize', updateUnderline);
return () => window.removeEventListener('resize', updateUnderline);
}, [activeIndex, finalItems, showLabels, orientation]);
const handleItemClick = (index, item) => {
setActiveIndex(index);
item.onClick?.();
};
const getVariantStyles = () => {
switch (variant) {
case 'compact':
return {
container: 'p-1',
item: 'p-2 min-w-12',
icon: 'h-4 w-4',
text: 'text-xs'
};
case 'large':
return {
container: 'p-3',
item: 'p-3 min-w-16',
icon: 'h-6 w-6',
text: 'text-base'
};
default:
return {
container: 'p-2',
item: 'p-2 min-w-14',
icon: 'h-5 w-5',
text: 'text-sm'
};
}
};
const styles = getVariantStyles();
return (
<nav
className={cn(
'relative inline-flex items-center rounded-xl bg-card border shadow-sm',
orientation === 'horizontal' ? 'flex-row' : 'flex-col',
styles.container,
className
)}
role="navigation">
{finalItems.map((item, index) => {
const isActive = index === activeIndex;
const IconComponent = item.icon;
return (
<button
key={`${item.label}-${index}`}
ref={(el) => { itemRefs.current[index] = el; }}
className={cn(
'relative flex flex-col items-center justify-center rounded-lg transition-all duration-200',
'hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
styles.item,
isActive && 'text-primary',
!isActive && 'text-muted-foreground hover:text-foreground'
)}
onClick={() => handleItemClick(index, item)}
aria-label={item.label}
type="button">
<div
className={cn(
'flex items-center justify-center transition-all duration-200',
animated && isActive && 'animate-bounce',
orientation === 'horizontal' && showLabels ? 'mb-1' : '',
orientation === 'vertical' && showLabels ? 'mb-1' : ''
)}>
<IconComponent className={cn(styles.icon, 'transition-colors duration-200')} />
</div>
{showLabels && (
<span
ref={(el) => { textRefs.current[index] = el; }}
className={cn(
'font-medium transition-colors duration-200 capitalize',
styles.text,
'whitespace-nowrap'
)}>
{item.label}
</span>
)}
</button>
);
})}
{/* Animated underline for horizontal orientation with labels */}
{showLabels && orientation === 'horizontal' && (
<div
className={cn(
'absolute bottom-2 h-0.5 bg-primary rounded-full transition-all duration-300 ease-out',
animated ? 'transition-all duration-300' : ''
)}
style={{
width: `${underlineWidth}px`,
left: `${underlineLeft}px`,
}} />
)}
{/* Active indicator for vertical orientation or no labels */}
{(!showLabels || orientation === 'vertical') && (
<div
className={cn(
'absolute bg-primary rounded-full transition-all duration-300',
orientation === 'vertical'
? 'left-1 w-1 h-6'
: 'bottom-0.5 h-0.5 w-6'
)}
style={{
[orientation === 'vertical' ? 'top' : 'left']:
orientation === 'vertical'
? `${(activeIndex * (variant === 'large' ? 64 : variant === 'compact' ? 56 : 60)) + (variant === 'large' ? 19 : variant === 'compact' ? 16 : 18)}px`
: `${(activeIndex * (variant === 'large' ? 64 : variant === 'compact' ? 56 : 60)) + (variant === 'large' ? 19 : variant === 'compact' ? 16 : 18)}px`
}} />
)}
</nav>
);
};

View File

@@ -0,0 +1,26 @@
'use client';
import { MenuDock } from '@/components/ui/shadcn-io/menu-dock';
import { Home, Settings, Bell } from 'lucide-react';
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
const sidebarItems = [
{ label: 'home', icon: Home, onClick: () => console.log('Home clicked') },
{ label: 'notifications', icon: Bell, onClick: () => console.log('Notifications clicked') },
{ label: 'settings', icon: Settings, onClick: () => console.log('Settings clicked') },
];
export default function MenuDockVertical() {
return (
<div className="min-h-[180px] p-4 flex justify-start items-start">
<div className="flex flex-col items-center justify-center">
<Avatar className="mb-2 size-10 justify-center items-center">
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<MenuDock
items={sidebarItems}
variant="compact"
orientation="vertical"
/>
</div>
</div>
);
}