start creating dashboard
This commit is contained in:
100
taskncoffee-app/package-lock.json
generated
100
taskncoffee-app/package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@fingerprintjs/fingerprintjs": "^4.6.2",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
@@ -1011,6 +1012,33 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-avatar": {
|
||||
"version": "1.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz",
|
||||
"integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-is-hydrated": "0.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "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
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
@@ -1026,6 +1054,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-label": {
|
||||
"version": "2.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz",
|
||||
@@ -1090,6 +1133,54 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-is-hydrated": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz",
|
||||
"integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "^1.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.38",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz",
|
||||
@@ -5463,6 +5554,15 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.1.9",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@fingerprintjs/fingerprintjs": "^4.6.2",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
45
taskncoffee-app/src/components/ui/avatar.jsx
Normal file
45
taskncoffee-app/src/components/ui/avatar.jsx
Normal 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 }
|
||||
185
taskncoffee-app/src/components/ui/shadcn-io/menu-dock/index.jsx
Normal file
185
taskncoffee-app/src/components/ui/shadcn-io/menu-dock/index.jsx
Normal 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>
|
||||
);
|
||||
};
|
||||
26
taskncoffee-app/src/layouts/DashLayout.jsx
Normal file
26
taskncoffee-app/src/layouts/DashLayout.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user