tahamajs/IE / Projects /front /src /components /layout /FloatingDock.tsx
tahamajs's picture
download
raw
24.3 kB
import { useState, useEffect } from "react";
import { NavLink, useLocation } from "react-router-dom";
import { motion, AnimatePresence } from "framer-motion";
import { toast } from "react-toastify";
import {
Home, Search, LayoutDashboard, BookOpen, Users, Briefcase, Settings, Bell,
HelpCircle, Sparkles, Package, Brain, MessageSquare, Video, BarChart3,
Award, Library, PenTool, Grid, ChevronUp, PanelLeft, Calendar, Activity,
Inbox, Globe, FileText, FolderOpen, FileUp, Zap, FlaskConical, Shield,
ShieldCheck, Tag, Heart, Wrench, Database
} from "lucide-react";
export type DockItem = {
to: string;
label: string;
icon: React.ComponentType<{ className?: string }>;
exact?: boolean;
};
const DEFAULT_DOCK_PATHS = [
"/",
"/dashboard",
"/articles",
"/groups",
"/projects",
"/discovery",
"/notifications",
"/settings",
];
export const ALL_AVAILABLE_APPS: DockItem[] = [
{ to: "/", label: "Home", icon: Home, exact: true },
{ to: "/dashboard", label: "Dashboard", icon: LayoutDashboard },
{ to: "/articles", label: "Articles", icon: BookOpen },
{ to: "/groups", label: "Groups", icon: Users },
{ to: "/projects", label: "Projects", icon: Briefcase },
{ to: "/discovery", label: "Explore", icon: Search },
{ to: "/notifications", label: "Alerts", icon: Bell },
{ to: "/settings", label: "Settings", icon: Settings },
{ to: "/workspace", icon: PanelLeft, label: "Workspace" },
{ to: "/calendar", icon: Calendar, label: "Calendar" },
{ to: "/conference", icon: Video, label: "Conference" },
{ to: "/roadmap", icon: Activity, label: "Roadmap" },
{ to: "/peer-room", icon: MessageSquare, label: "Peer Room" },
{ to: "/inbox", icon: Inbox, label: "Inbox" },
{ to: "/messages", icon: MessageSquare, label: "Messages" },
{ to: "/networking", icon: Globe, label: "Networking" },
{ to: "/my-posts", icon: PenTool, label: "My Posts" },
{ to: "/collections", icon: FolderOpen, label: "Collections" },
{ to: "/submit-preprint", icon: FileUp, label: "Preprint" },
{ to: "/editor", icon: BookOpen, label: "Sci Editor" },
{ to: "/authoring-studio", icon: PenTool, label: "Authoring" },
{ to: "/ai-co-scientist", icon: Brain, label: "AI Co-Scientist" },
{ to: "/gap-analysis", icon: Activity, label: "Gap Analysis" },
{ to: "/grant-assistant", icon: Award, label: "Grants" },
{ to: "/research-assistant", icon: Zap, label: "Assistant" },
{ to: "/lab-inventory", icon: FlaskConical, label: "Lab" },
{ to: "/peer-review", icon: Shield, label: "Review" },
{ to: "/trust-dashboard", icon: ShieldCheck, label: "Trust Dashboard" },
{ to: "/analytics", icon: BarChart3, label: "Analytics" },
{ to: "/tags", icon: Tag, label: "Tags" },
{ to: "/recommendations", icon: Heart, label: "Recommendations" },
{ to: "/tools", icon: Wrench, label: "App Store" },
{ to: "/tools/builder", icon: Package, label: "Tool Studio" },
{ to: "/tools/installed", icon: Package, label: "Installed Tools" },
{ to: "/data-hub", icon: Database, label: "Data Hub" },
{ to: "/api-docs", icon: FileText, label: "API Docs" },
{ to: "/status", icon: Activity, label: "Status" },
];
export const SECTIONS = [
{
id: "main",
label: "Main",
items: [
{ to: "/dashboard", icon: LayoutDashboard, label: "Dashboard", description: "Your research hub" },
{ to: "/workspace", icon: PanelLeft, label: "Workspace", description: "Multi-panel view" },
{ to: "/calendar", icon: Calendar, label: "Calendar", description: "Event schedules" },
{ to: "/conference", icon: Video, label: "Conference", description: "Virtual conferences" },
{ to: "/roadmap", icon: Activity, label: "Roadmap", description: "Future plans" },
]
},
{
id: "communication",
label: "Network",
items: [
{ to: "/peer-room", icon: MessageSquare, label: "Peer Room", description: "Peer collaboration" },
{ to: "/inbox", icon: Inbox, label: "Inbox", description: "All conversations" },
{ to: "/messages", icon: MessageSquare, label: "Messages", description: "Chat & groups" },
{ to: "/groups", icon: Users, label: "Groups", description: "Research groups" },
{ to: "/networking", icon: Globe, label: "Networking", description: "Connect with peers" },
]
},
{
id: "content",
label: "Content",
items: [
{ to: "/articles", icon: FileText, label: "Articles", description: "Discover papers" },
{ to: "/my-posts", icon: PenTool, label: "My Posts", description: "Manage posts" },
{ to: "/collections", icon: FolderOpen, label: "Collections", description: "Saved papers" },
{ to: "/submit-preprint", icon: FileUp, label: "Preprint", description: "Submit paper" },
{ to: "/editor", icon: BookOpen, label: "Sci Editor", description: "Write papers with LaTeX" },
{ to: "/authoring-studio", icon: PenTool, label: "Authoring", description: "Write & edit" },
]
},
{
id: "lab",
label: "Lab & AI",
items: [
{ to: "/ai-co-scientist", icon: Brain, label: "AI Co-Scientist", description: "Virtual collaborator" },
{ to: "/gap-analysis", icon: Activity, label: "Gap Analysis", description: "Research gaps" },
{ to: "/grant-assistant", icon: Award, label: "Grants", description: "Proposal writing" },
{ to: "/research-assistant", icon: Zap, label: "Research Assistant", description: "AI-powered help" },
{ to: "/lab-inventory", icon: FlaskConical, label: "Lab", description: "Inventory & safety" },
{ to: "/peer-review", icon: Shield, label: "Review", description: "Anonymous peer review" },
{ to: "/projects", icon: Briefcase, label: "Projects", description: "Manage projects" },
{ to: "/trust-dashboard", icon: ShieldCheck, label: "Trust Dashboard", description: "Security & verify" },
]
},
{
id: "data",
label: "Data",
items: [
{ to: "/analytics", icon: BarChart3, label: "Analytics", description: "Research metrics" },
{ to: "/discovery", icon: Search, label: "Discovery", description: "Find trends" },
{ to: "/tags", icon: Tag, label: "Tags", description: "Browse by topic" },
{ to: "/recommendations", icon: Heart, label: "Recommendations", description: "Personalized" },
]
},
{
id: "tools",
label: "Tools",
items: [
{ to: "/tools", icon: Wrench, label: "App Store", description: "Research tools" },
{ to: "/tools/builder", icon: Package, label: "Build Tool Studio", description: "Visual editor" },
{ to: "/tools/installed", icon: Package, label: "Installed Tools", description: "Manage add-ons" },
{ to: "/data-hub", icon: Database, label: "Data Hub", description: "Research data" },
{ to: "/api-docs", icon: FileText, label: "API Docs", description: "Developer resources" },
{ to: "/status", icon: Activity, label: "Status", description: "Service status" },
]
}
];
export function FloatingDock() {
const [isMoreOpen, setIsMoreOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const location = useLocation();
const [dockItems, setDockItems] = useState<DockItem[]>([]);
// Load custom dock items
useEffect(() => {
const handleStorageChange = () => {
try {
const customPaths = localStorage.getItem("ie-custom-dock-items");
const paths = customPaths ? JSON.parse(customPaths) : DEFAULT_DOCK_PATHS;
const mappedItems = paths.map((p: string) => {
return ALL_AVAILABLE_APPS.find(a => a.to === p);
}).filter(Boolean) as DockItem[];
setDockItems(mappedItems.length > 0 ? mappedItems : ALL_AVAILABLE_APPS.slice(0, 8));
} catch {
setDockItems(ALL_AVAILABLE_APPS.slice(0, 8));
}
};
handleStorageChange();
window.addEventListener("dock-settings-changed", handleStorageChange);
return () => window.removeEventListener("dock-settings-changed", handleStorageChange);
}, []);
useEffect(() => {
setIsMoreOpen(false);
setSearchQuery("");
}, [location.pathname]);
const isActivePath = (item: DockItem) => {
if (item.exact) return location.pathname === item.to;
return location.pathname.startsWith(item.to) && (location.pathname === item.to || location.pathname.charAt(item.to.length) === '/');
};
const hasAnyMatches = SECTIONS.some(section =>
section.items.some(item =>
item.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.description.toLowerCase().includes(searchQuery.toLowerCase())
)
);
return (
<div className="fixed inset-x-0 bottom-6 z-50 px-4 pointer-events-none flex justify-center">
<div className="relative flex flex-col items-center justify-center gap-4 max-w-full w-auto">
{/* Expanded Popover Menu */}
<AnimatePresence>
{isMoreOpen && (
<motion.div
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 20, scale: 0.95 }}
transition={{ type: "spring", bounce: 0.3, duration: 0.5 }}
className="pointer-events-auto w-[90vw] max-w-3xl rounded-[2.5rem] premium-glass bg-white/80 p-6 shadow-[0_30px_100px_rgba(0,0,0,0.15)] backdrop-blur-3xl dark:border-white/10 dark:bg-slate-950/80 border border-white/60 dark:shadow-[0_30px_100px_rgba(0,0,0,0.5)]"
>
<div className="mb-5 flex flex-col md:flex-row md:items-center justify-between border-b border-slate-200/50 pb-4 dark:border-slate-800/50 px-2 gap-3">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-xl bg-indigo-500/10 border border-indigo-500/20">
<Sparkles className="h-5 w-5 text-indigo-500" />
</div>
<h3 className="font-bold text-lg text-slate-800 dark:text-white">Workspace Apps</h3>
</div>
{/* Search Bar */}
<div className="relative flex-1 max-w-xs md:ml-4">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
<input
type="text"
placeholder="Search apps..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-9 pr-10 py-1.5 rounded-xl border border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-900/60 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 text-slate-800 dark:text-white"
/>
{searchQuery && (
<button
onClick={() => setSearchQuery("")}
className="absolute right-3 top-1/2 -translate-y-1/2 text-xs font-bold text-slate-450 hover:text-slate-600 dark:hover:text-slate-200"
>
Clear
</button>
)}
</div>
<span className="text-xs font-medium text-slate-400 bg-slate-100 dark:bg-slate-800 px-3 py-1 rounded-full border border-slate-200 dark:border-slate-700 self-start md:self-auto">All Modules</span>
</div>
<div className="max-h-[60vh] overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-slate-300 dark:scrollbar-thumb-slate-700 scrollbar-track-transparent">
{SECTIONS.map((section, sIdx) => {
const filteredItems = section.items.filter(item =>
item.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.description.toLowerCase().includes(searchQuery.toLowerCase())
);
if (filteredItems.length === 0) return null;
return (
<div key={section.id} className="mb-6 last:mb-0">
<h4 className="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider mb-3 px-2 flex items-center gap-2">
<div className="w-4 h-[1px] bg-slate-300 dark:bg-slate-700" />
{section.label}
<div className="flex-1 h-[1px] bg-slate-200 dark:bg-slate-800" />
</h4>
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-4">
{filteredItems.map((item, idx) => (
<motion.div
key={item.to}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: (sIdx * 0.05) + (idx * 0.02) }}
>
<NavLink
to={item.to}
onClick={(e) => {
if (location.pathname === "/workspace") {
// Map routes to workspace panel types
const routeToPanel: Record<string, string> = {
"/ai-co-scientist": "ai-chat",
"/editor": "collaborative-editor",
"/lab-inventory": "lab-inventory",
"/gap-analysis": "gap-analysis",
"/grant-assistant": "grant-assistant",
"/research-assistant": "research-assistant",
"/messages": "messages",
"/articles": "articles",
"/tools": "tools",
"/tools/builder": "tool-builder",
"/tools/installed": "installed-tools",
"/status": "status"
};
const panelType = routeToPanel[item.to];
if (panelType) {
e.preventDefault();
window.dispatchEvent(new CustomEvent("workspace:open-panel", { detail: { type: panelType } }));
setIsMoreOpen(false);
toast.info(`Opened ${item.label} in Workspace 💻`, { position: "bottom-right", autoClose: 2000 });
}
}
}}
className="group flex flex-col items-center justify-center rounded-[1.25rem] border border-transparent bg-white/60 p-3 text-center transition-all duration-300 hover:border-indigo-500/30 hover:bg-white hover:shadow-xl dark:bg-slate-900/60 dark:hover:bg-slate-800 hover:-translate-y-1 relative overflow-hidden h-full"
>
<div className="absolute inset-0 bg-gradient-to-br from-indigo-500/5 to-purple-500/5 opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="mb-2.5 flex h-10 w-10 items-center justify-center rounded-2xl bg-gradient-to-br from-indigo-500/10 to-purple-500/10 text-indigo-600 dark:from-indigo-400/20 dark:to-purple-400/20 dark:text-indigo-300 border border-indigo-500/10 shadow-inner group-hover:scale-110 transition-transform duration-300">
<item.icon className="h-5 w-5" />
</div>
<span className="text-[11px] font-bold text-slate-800 dark:text-white relative z-10 leading-tight">{item.label}</span>
<span className="mt-1 hidden text-[9px] leading-tight text-slate-500 dark:text-slate-400 sm:block relative z-10">{item.description}</span>
</NavLink>
</motion.div>
))}
</div>
</div>
);
})}
{!hasAnyMatches && (
<div className="flex flex-col items-center justify-center py-12 text-slate-500 dark:text-slate-400">
<Package className="h-12 w-12 text-slate-400 mb-3 opacity-55 animate-bounce" />
<p className="text-sm font-semibold">No apps match "{searchQuery}"</p>
<button
onClick={() => setSearchQuery("")}
className="mt-2 text-xs font-bold text-indigo-500 hover:underline"
>
Clear search query
</button>
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
{/* Main Dock Bar */}
<motion.nav
layout
className="pointer-events-auto flex w-full max-w-full items-center justify-between gap-1 overflow-x-auto rounded-[2rem] bg-white/80 px-2 py-2 shadow-[0_20px_60px_rgba(15,23,42,0.12),0_0_0_1px_rgba(255,255,255,0.4)_inset] backdrop-blur-3xl dark:bg-slate-950/80 dark:shadow-[0_20px_60px_rgba(0,0,0,0.5),0_0_0_1px_rgba(255,255,255,0.05)_inset] border border-slate-200/50 dark:border-white/10 scrollbar-hide"
>
{dockItems.map((item) => {
const isActive = isActivePath(item) && !isMoreOpen;
return (
<NavLink
key={item.to}
to={item.to}
onClick={(e) => {
if (location.pathname === "/workspace") {
const routeToPanel: Record<string, string> = {
"/ai-co-scientist": "ai-chat",
"/editor": "collaborative-editor",
"/lab-inventory": "lab-inventory",
"/gap-analysis": "gap-analysis",
"/grant-assistant": "grant-assistant",
"/research-assistant": "research-assistant",
"/messages": "messages",
"/articles": "articles",
"/tools": "tools",
"/tools/builder": "tool-builder",
"/tools/installed": "installed-tools",
"/status": "status"
};
const panelType = routeToPanel[item.to];
if (panelType) {
e.preventDefault();
window.dispatchEvent(new CustomEvent("workspace:open-panel", { detail: { type: panelType } }));
toast.info(`Opened ${item.label} in Workspace 💻`, { position: "bottom-right", autoClose: 2000 });
}
}
}}
className="relative group flex min-w-[3.5rem] flex-1 items-center justify-center gap-2 rounded-full px-4 py-2.5 text-xs font-bold transition-colors duration-300"
>
{/* Windows-style Hover Preview */}
<div className="absolute bottom-[calc(100%+16px)] left-1/2 -translate-x-1/2 opacity-0 scale-95 pointer-events-none group-hover:opacity-100 group-hover:scale-100 transition-all duration-300 origin-bottom z-50">
<div className="premium-glass bg-white/90 dark:bg-slate-900/90 backdrop-blur-xl border border-slate-200/50 dark:border-white/10 shadow-2xl rounded-2xl p-3 w-48 flex flex-col gap-2">
<div className="flex items-center gap-2 mb-1 border-b border-slate-100 dark:border-slate-800 pb-2">
<div className="p-1.5 rounded-lg bg-indigo-500/10 text-indigo-500 dark:text-indigo-400">
<item.icon className="h-4 w-4" />
</div>
<span className="font-bold text-sm text-slate-800 dark:text-white truncate">{item.label}</span>
</div>
{/* Fake window content preview */}
<div className="w-full h-24 bg-slate-50 dark:bg-slate-950 rounded-lg border border-slate-200/50 dark:border-white/5 overflow-hidden shadow-inner flex flex-col">
<div className="h-4 w-full bg-slate-200/50 dark:bg-slate-800/50 flex items-center px-1.5 gap-1 border-b border-slate-200/50 dark:border-white/5">
<div className="w-1.5 h-1.5 rounded-full bg-red-400/80"></div>
<div className="w-1.5 h-1.5 rounded-full bg-amber-400/80"></div>
<div className="w-1.5 h-1.5 rounded-full bg-emerald-400/80"></div>
</div>
<div className="p-2 space-y-1.5 flex-1 opacity-70">
<div className="w-3/4 h-1.5 bg-slate-200 dark:bg-slate-700 rounded-full"></div>
<div className="w-1/2 h-1.5 bg-slate-200 dark:bg-slate-700 rounded-full"></div>
<div className="w-5/6 h-1.5 bg-slate-200 dark:bg-slate-700 rounded-full"></div>
<div className="w-2/3 h-1.5 bg-slate-200 dark:bg-slate-700 rounded-full"></div>
</div>
</div>
</div>
{/* Arrow pointing down */}
<div className="absolute -bottom-2 left-1/2 -translate-x-1/2 w-4 h-4 rotate-45 bg-white/90 dark:bg-slate-900/90 border-b border-r border-slate-200/50 dark:border-white/10 shadow-[4px_4px_10px_rgba(0,0,0,0.1)]"></div>
</div>
{isActive && (
<motion.div
layoutId="activeDockPill"
className="absolute inset-0 rounded-full bg-gradient-to-r from-indigo-600 to-purple-600 shadow-md"
transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
/>
)}
<div className={`relative z-10 flex items-center gap-2 ${isActive ? "text-white" : "text-slate-600 dark:text-slate-400 group-hover:text-indigo-600 dark:group-hover:text-indigo-400"}`}>
<item.icon className={`h-4 w-4 shrink-0 transition-transform duration-300 ${isActive ? "scale-110" : "group-hover:scale-110 group-hover:-translate-y-0.5"}`} />
<span className="hidden md:inline">{item.label}</span>
</div>
{!isActive && (
<div className="absolute inset-0 rounded-full bg-slate-100 dark:bg-white/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
)}
</NavLink>
);
})}
{/* Vertical Divider */}
<div className="w-px h-6 bg-slate-300 dark:bg-slate-700 mx-1 rounded-full opacity-50" />
{/* Toggle More Menu Button */}
<button
onClick={() => setIsMoreOpen(!isMoreOpen)}
className="relative group flex min-w-[3.5rem] items-center justify-center gap-2 rounded-full px-4 py-2.5 text-xs font-bold transition-colors duration-300"
>
{isMoreOpen && (
<motion.div
layoutId="activeDockPill"
className="absolute inset-0 rounded-full bg-gradient-to-r from-indigo-600 to-purple-600 shadow-md"
transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
/>
)}
<div className={`relative z-10 flex items-center gap-2 ${isMoreOpen ? "text-white" : "text-slate-600 dark:text-slate-400 group-hover:text-indigo-600 dark:group-hover:text-indigo-400"}`}>
<Grid className={`h-4 w-4 shrink-0 transition-transform duration-300 ${isMoreOpen ? "scale-110" : "group-hover:scale-110 group-hover:-translate-y-0.5"}`} />
<span className="hidden md:inline">Apps</span>
<ChevronUp className={`hidden sm:block h-3 w-3 shrink-0 transition-transform duration-500 ${isMoreOpen ? "rotate-180" : ""}`} />
</div>
{!isMoreOpen && (
<div className="absolute inset-0 rounded-full bg-slate-100 dark:bg-white/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
)}
</button>
</motion.nav>
</div>
</div>
);
}

Xet Storage Details

Size:
24.3 kB
·
Xet hash:
ab32395cae87557cb36521f7c3b9f80ba2fe9d4ef96c326bd12d438250172463

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.