ishaq101's picture
[NOTICKET] Branding Styling and Integration (Chat & Voice)
5ca6cf1
import { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
import {
Plus,
MessageSquare,
Trash2,
ChevronLeft,
ChevronRight,
Settings,
LogOut,
} from "lucide-react";
import maintivalogoUrl from "../../../assets/maintiva-logo.jpg";
import type { ChatSession, StoredUser } from "./types";
interface SidebarProps {
sessions: ChatSession[];
activeSessionId: string | null;
isLoadingSessions: boolean;
user: StoredUser | null;
onNewChat: () => void;
onSelectSession: (id: string) => void;
onDeleteSession: (id: string) => void;
onLogout: () => void;
mobileOpen: boolean;
onMobileClose: () => void;
}
function formatTime(dateStr: string | null | undefined): string {
if (!dateStr) return "";
const d = new Date(dateStr);
const now = new Date();
const diffMs = now.getTime() - d.getTime();
const diffDays = Math.floor(diffMs / 86400000);
if (diffDays === 0) {
return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
}
if (diffDays === 1) return "Yesterday";
if (diffDays < 7) return `${diffDays}d ago`;
return d.toLocaleDateString([], { month: "short", day: "numeric" });
}
export default function Sidebar({
sessions,
activeSessionId,
isLoadingSessions,
user,
onNewChat,
onSelectSession,
onDeleteSession,
onLogout,
mobileOpen,
onMobileClose,
}: SidebarProps) {
const [collapsed, setCollapsed] = useState(false);
const [hoveredSession, setHoveredSession] = useState<string | null>(null);
return (
<>
{/* Mobile backdrop */}
<AnimatePresence>
{mobileOpen && (
<motion.div
className="fixed inset-0 bg-black/40 z-40 md:hidden"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
onClick={onMobileClose}
/>
)}
</AnimatePresence>
<motion.div
className={[
"flex flex-col h-full flex-shrink-0 bg-white border-r border-neutral-100 overflow-hidden",
// Mobile: fixed overlay drawer; desktop: in-flow
"fixed inset-y-0 left-0 z-50 md:relative md:inset-auto md:z-auto",
// Mobile slide in/out; always visible on desktop
mobileOpen ? "translate-x-0" : "-translate-x-full",
"md:translate-x-0",
// CSS transition for mobile only; framer-motion handles desktop width
"transition-transform duration-300 ease-in-out md:transition-none",
// Never overflow phone screen
"max-w-[85vw] md:max-w-none",
].join(" ")}
animate={{ width: collapsed ? 72 : 280 }}
transition={{ duration: 0.3, ease: "easeInOut" }}
>
{/* Collapse toggle — desktop only */}
<button
onClick={() => setCollapsed((v) => !v)}
className="hidden md:flex absolute -right-3 top-1/2 -translate-y-1/2 z-20 w-6 h-6 rounded-full bg-white border border-neutral-200 shadow-sm text-neutral-500 hover:text-brand-green hover:border-brand-green transition-all items-center justify-center"
aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
>
{collapsed ? (
<ChevronRight className="h-3.5 w-3.5" />
) : (
<ChevronLeft className="h-3.5 w-3.5" />
)}
</button>
{/* Header */}
<div className="flex items-center gap-3 px-4 py-4 border-b border-neutral-100">
<motion.div
className="w-9 h-9 rounded-xl overflow-hidden flex-shrink-0 shadow-md"
whileHover={{ scale: 1.05 }}
>
<img src={maintivalogoUrl} alt="Maintiva" className="w-full h-full object-cover" />
</motion.div>
<AnimatePresence>
{!collapsed && (
<motion.div
initial={{ opacity: 0, width: 0 }}
animate={{ opacity: 1, width: "auto" }}
exit={{ opacity: 0, width: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
<p className="font-bold text-neutral-900 text-base whitespace-nowrap">Maintiva Agent</p>
<p className="text-xs text-neutral-400 whitespace-nowrap">AI Data Assistant</p>
</motion.div>
)}
</AnimatePresence>
</div>
{/* New Chat button */}
<div className="px-3 py-3">
<button
onClick={() => { onNewChat(); onMobileClose(); }}
className={`w-full flex items-center gap-3 rounded-xl px-3 py-2.5 bg-gradient-to-br from-brand-green-light to-brand-green text-white text-sm font-semibold shadow-md shadow-brand-green/20 hover:shadow-lg hover:shadow-brand-green/25 hover:brightness-105 transition-all ${
collapsed ? "justify-center px-0" : ""
}`}
>
<Plus className="h-4 w-4 flex-shrink-0" />
<AnimatePresence>
{!collapsed && (
<motion.span
initial={{ opacity: 0, width: 0 }}
animate={{ opacity: 1, width: "auto" }}
exit={{ opacity: 0, width: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden whitespace-nowrap"
>
New Chat
</motion.span>
)}
</AnimatePresence>
</button>
</div>
{/* Sessions */}
<div className="flex-1 overflow-y-auto">
{!collapsed && sessions.length > 0 && (
<p className="text-xs font-semibold text-neutral-400 uppercase tracking-wider px-4 pt-2 pb-1">
Recent
</p>
)}
{isLoadingSessions ? (
<div className="flex justify-center py-4">
<div className="w-4 h-4 rounded-full border-2 border-brand-green border-t-transparent animate-spin" />
</div>
) : (
<div className="px-2 space-y-0.5">
{sessions.map((session) => {
const isActive = session.id === activeSessionId;
return (
<div
key={session.id}
className={`relative flex items-center gap-3 rounded-xl px-3 py-2.5 cursor-pointer transition-all duration-150 ${
isActive
? "bg-brand-green-50 text-brand-green"
: "text-neutral-700 hover:bg-neutral-50"
}`}
onClick={() => { onSelectSession(session.id); onMobileClose(); }}
onMouseEnter={() => setHoveredSession(session.id)}
onMouseLeave={() => setHoveredSession(null)}
>
<MessageSquare
className={`h-4 w-4 flex-shrink-0 ${
isActive ? "text-brand-green" : "text-neutral-400"
}`}
/>
<AnimatePresence>
{!collapsed && (
<motion.div
initial={{ opacity: 0, width: 0 }}
animate={{ opacity: 1, width: "auto" }}
exit={{ opacity: 0, width: 0 }}
transition={{ duration: 0.2 }}
className="flex-1 min-w-0 overflow-hidden"
>
<p className="text-sm font-medium truncate">{session.title}</p>
<p className="text-xs text-neutral-400 truncate">
{formatTime(session.updatedAt ?? session.createdAt)}
</p>
</motion.div>
)}
</AnimatePresence>
{!collapsed && hoveredSession === session.id && (
<button
onClick={(e) => {
e.stopPropagation();
onDeleteSession(session.id);
}}
className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 rounded-lg text-neutral-400 hover:text-red-500 hover:bg-red-50 transition-all"
aria-label="Delete session"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
)}
</div>
);
})}
</div>
)}
</div>
{/* Footer */}
<div className="border-t border-neutral-100 p-3">
<div className="flex items-center gap-3 rounded-xl px-2 py-2">
<div className="h-7 w-7 rounded-full bg-gradient-to-br from-brand-green-light to-brand-green flex items-center justify-center flex-shrink-0">
<span className="text-xs font-bold text-white">
{user?.name?.[0]?.toUpperCase() ?? "U"}
</span>
</div>
<AnimatePresence>
{!collapsed && (
<motion.div
initial={{ opacity: 0, width: 0 }}
animate={{ opacity: 1, width: "auto" }}
exit={{ opacity: 0, width: 0 }}
transition={{ duration: 0.2 }}
className="flex-1 min-w-0 overflow-hidden"
>
<p className="text-sm font-semibold text-neutral-900 truncate whitespace-nowrap">
{user?.name ?? "User"}
</p>
<p className="text-xs text-neutral-400 truncate whitespace-nowrap">
{user?.email ?? ""}
</p>
</motion.div>
)}
</AnimatePresence>
{!collapsed && (
<div className="flex items-center gap-0.5">
<button
className="p-1.5 rounded-lg text-neutral-400 hover:text-brand-green hover:bg-brand-green-50 transition-all"
aria-label="Settings"
>
<Settings className="h-4 w-4" />
</button>
<button
onClick={onLogout}
className="p-1.5 rounded-lg text-neutral-400 hover:text-red-500 hover:bg-red-50 transition-all"
aria-label="Logout"
>
<LogOut className="h-4 w-4" />
</button>
</div>
)}
{collapsed && (
<button
onClick={onLogout}
className="p-1.5 rounded-lg text-neutral-400 hover:text-red-500 hover:bg-red-50 transition-all"
aria-label="Logout"
>
<LogOut className="h-4 w-4" />
</button>
)}
</div>
</div>
</motion.div>
</>
);
}