Download frontend/src/components/Sidebar.jsx from MoBkr/dosemate: direct link, hf CLI and curl.
- Browser
- Download file 10.7 kB
-
https://huggingface.co/spaces/MoBkr/dosemate/resolve/main/frontend/src/components/Sidebar.jsx
- Command line
-
hf download hf://spaces/MoBkr/dosemate/frontend/src/components/Sidebar.jsx
-
curl -L -o Sidebar.jsx https://huggingface.co/spaces/MoBkr/dosemate/resolve/main/frontend/src/components/Sidebar.jsx
10.7 kB
| import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from "react" | |
| import { useNavigate, useLocation } from "react-router-dom" | |
| import { | |
| MessageSquare, Plus, LayoutDashboard, CalendarClock, CheckSquare, | |
| Trophy, User, LogOut, Trash2, MoreVertical, HelpCircle, BookOpen, Menu, X, | |
| } from "lucide-react" | |
| import { getConversations, createConversation } from "../api" | |
| import { useAuth } from "../auth/AuthContext" | |
| import { useI18n } from "../i18n/I18nContext" | |
| import { useIsMobile } from "../useMediaQuery" | |
| import HelpGuide from "./HelpGuide" | |
| import { T } from "../theme" | |
| const Sidebar = forwardRef(function Sidebar( | |
| { activeConversationId, onSelectConversation, onNewConversation, onDeleteConversation }, | |
| ref | |
| ) { | |
| const [conversations, setConversations] = useState([]) | |
| const [menu, setMenu] = useState(null) | |
| const [showHelp, setShowHelp] = useState(false) | |
| const [drawerOpen, setDrawerOpen] = useState(false) | |
| const navigate = useNavigate() | |
| const location = useLocation() | |
| const isMobile = useIsMobile() | |
| const { logout, user } = useAuth() | |
| const { t } = useI18n() | |
| const topNav = [ | |
| { icon: LayoutDashboard, label: t("nav_dashboard"), path: "/dashboard" }, | |
| { icon: MessageSquare, label: t("nav_chat"), path: "/chat" }, | |
| { icon: CalendarClock, label: t("nav_schedule"), path: "/schedule" }, | |
| { icon: CheckSquare, label: t("nav_today"), path: "/today" }, | |
| { icon: BookOpen, label: t("nav_medicines"), path: "/medicines" }, | |
| { icon: Trophy, label: t("nav_leaderboard"), path: "/leaderboard" }, | |
| { icon: User, label: t("nav_profile"), path: "/profile" }, | |
| ] | |
| useImperativeHandle(ref, () => ({ | |
| refresh: loadConversations, | |
| updateTitle: (id, title) => | |
| setConversations((prev) => prev.map((c) => (c.id === id ? { ...c, title } : c))), | |
| })) | |
| useEffect(() => { loadConversations() }, []) | |
| useEffect(() => { loadConversations() }, [activeConversationId]) | |
| useEffect(() => { setDrawerOpen(false) }, [location.pathname]) // close on navigation | |
| useEffect(() => { | |
| const close = () => setMenu(null) | |
| document.addEventListener("click", close) | |
| return () => document.removeEventListener("click", close) | |
| }, []) | |
| const loadConversations = async () => { | |
| try { setConversations(await getConversations()) } catch (e) { console.error(e) } | |
| } | |
| const go = (path) => { navigate(path); setDrawerOpen(false) } | |
| const handleNew = async () => { | |
| if (onNewConversation) { | |
| const id = await createConversation() | |
| await loadConversations() | |
| onNewConversation(id) | |
| } else { | |
| navigate("/chat") | |
| } | |
| setDrawerOpen(false) | |
| } | |
| const handleSelect = (id) => { | |
| if (onSelectConversation) onSelectConversation(id) | |
| else navigate("/chat") | |
| setDrawerOpen(false) | |
| } | |
| const openMenu = (e, convId) => { | |
| e.stopPropagation() | |
| const r = e.currentTarget.getBoundingClientRect() | |
| setMenu({ x: Math.min(r.left, window.innerWidth - 170), y: r.bottom + 4, convId }) | |
| } | |
| const handleDelete = async () => { | |
| if (!menu) return | |
| if (onDeleteConversation) await onDeleteConversation(menu.convId) | |
| await loadConversations() | |
| setMenu(null) | |
| } | |
| const panel = ( | |
| <> | |
| <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "2px 6px 0" }}> | |
| <div style={logo}>💊</div> | |
| <div> | |
| <div style={{ fontSize: 16, fontWeight: 800, color: T.text, lineHeight: 1 }}>DoseMate</div> | |
| <div style={{ fontSize: 9.5, color: T.faint, letterSpacing: ".1em", marginTop: 3 }}>{t("portal_subtitle")}</div> | |
| </div> | |
| </div> | |
| <button onClick={handleNew} style={newBtn}><Plus size={16} /> New Chat</button> | |
| <div style={{ display: "flex", flexDirection: "column", gap: 3 }}> | |
| {topNav.map(({ icon: Icon, label, path }) => { | |
| const active = location.pathname === path | |
| return ( | |
| <div key={path} onClick={() => go(path)} style={navItem(active)}> | |
| <Icon size={17} /> {label} | |
| </div> | |
| ) | |
| })} | |
| </div> | |
| <div style={{ marginTop: 18, flex: 1, overflowY: "auto", minHeight: 0 }}> | |
| <div style={sectionLabel}>RECENT CHATS</div> | |
| <div style={{ display: "flex", flexDirection: "column", gap: 2 }}> | |
| {conversations.map((conv) => { | |
| const active = activeConversationId === conv.id | |
| return ( | |
| <div key={conv.id} onClick={() => handleSelect(conv.id)} style={chatRow(active)}> | |
| <MessageSquare size={13} style={{ flexShrink: 0 }} /> | |
| <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: 1 }}> | |
| {conv.title || `Consultation #${conv.number ?? conv.id}`} | |
| </span> | |
| <button onClick={(e) => openMenu(e, conv.id)} style={dotsBtn}><MoreVertical size={15} /></button> | |
| </div> | |
| ) | |
| })} | |
| </div> | |
| </div> | |
| <div style={{ marginTop: "auto", paddingTop: 10, borderTop: `1px solid ${T.border}` }}> | |
| <div onClick={() => { setShowHelp(true); setDrawerOpen(false) }} style={navItem(false)}> | |
| <HelpCircle size={17} /> {t("help")} | |
| </div> | |
| <div style={userRow}> | |
| <div style={avatar}>{(user?.name || "U").charAt(0).toUpperCase()}</div> | |
| <div style={{ flex: 1, overflow: "hidden" }}> | |
| <div style={{ fontSize: 12.5, fontWeight: 600, color: T.text, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{user?.name || "User"}</div> | |
| <div style={{ fontSize: 10.5, color: T.faint, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{user?.email}</div> | |
| </div> | |
| <button onClick={logout} style={logoutBtn} title={t("nav_logout")}><LogOut size={15} /></button> | |
| </div> | |
| </div> | |
| </> | |
| ) | |
| const menuEl = menu && ( | |
| <div style={{ position: "fixed", top: menu.y, left: menu.x, ...menuBox }}> | |
| <div onClick={handleDelete} style={menuItem} | |
| onMouseEnter={(e) => (e.currentTarget.style.background = "#fef2f2")} | |
| onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}> | |
| <Trash2 size={14} /> Delete chat | |
| </div> | |
| </div> | |
| ) | |
| // ---------- Mobile: top bar + slide-in drawer ---------- | |
| if (isMobile) { | |
| return ( | |
| <> | |
| <div style={topbar}> | |
| <button onClick={() => setDrawerOpen(true)} style={hamburger} aria-label="menu"><Menu size={20} /></button> | |
| <div style={{ display: "flex", alignItems: "center", gap: 8 }}> | |
| <span style={{ fontSize: 18 }}>💊</span> | |
| <span style={{ fontWeight: 800, color: T.text, fontSize: 16 }}>DoseMate</span> | |
| </div> | |
| <div style={{ width: 38 }} /> | |
| </div> | |
| {drawerOpen && <div style={backdrop} onClick={() => setDrawerOpen(false)} />} | |
| <div style={{ ...drawer, transform: drawerOpen ? "translateX(0)" : "translateX(-105%)" }}> | |
| <button onClick={() => setDrawerOpen(false)} style={drawerClose}><X size={18} /></button> | |
| {panel} | |
| </div> | |
| {menuEl} | |
| {showHelp && <HelpGuide onClose={() => setShowHelp(false)} />} | |
| </> | |
| ) | |
| } | |
| // ---------- Desktop: fixed in-flow sidebar ---------- | |
| return ( | |
| <div style={wrap}> | |
| {panel} | |
| {menuEl} | |
| {showHelp && <HelpGuide onClose={() => setShowHelp(false)} />} | |
| </div> | |
| ) | |
| }) | |
| const wrap = { width: 240, height: "100%", background: T.surface, borderInlineEnd: `1px solid ${T.border}`, display: "flex", flexDirection: "column", padding: "20px 14px", flexShrink: 0, gap: 16 } | |
| const drawer = { position: "fixed", insetInlineStart: 0, top: 0, height: "100%", width: 270, maxWidth: "82vw", background: T.surface, display: "flex", flexDirection: "column", padding: "18px 14px", gap: 14, zIndex: 1600, boxShadow: T.shadowLg, transition: "transform .25s ease", overflowY: "auto" } | |
| const drawerClose = { position: "absolute", insetInlineEnd: 10, top: 10, background: "transparent", border: "none", color: T.faint, cursor: "pointer", padding: 4 } | |
| const backdrop = { position: "fixed", inset: 0, background: "rgba(15,23,42,.45)", zIndex: 1550 } | |
| const topbar = { position: "fixed", top: 0, insetInlineStart: 0, right: 0, height: 52, background: T.surface, borderBottom: `1px solid ${T.border}`, display: "flex", alignItems: "center", justifyContent: "space-between", padding: "0 12px", zIndex: 1400 } | |
| const hamburger = { width: 38, height: 38, borderRadius: 10, background: "#f0fdf4", border: "1px solid #bbf7d0", color: T.green, display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer" } | |
| const logo = { width: 38, height: 38, borderRadius: 12, background: T.gradient, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 19, boxShadow: T.shadowGreen } | |
| const newBtn = { display: "flex", alignItems: "center", justifyContent: "center", gap: 7, padding: "11px", borderRadius: 12, background: T.gradient, color: "#fff", border: "none", fontSize: 13.5, fontWeight: 600, cursor: "pointer", boxShadow: T.shadowGreen } | |
| const navItem = (active) => ({ display: "flex", alignItems: "center", gap: 11, padding: "10px 12px", borderRadius: 11, cursor: "pointer", fontSize: 13.5, fontWeight: active ? 700 : 500, background: active ? "#f0fdf4" : "transparent", color: active ? T.green : T.muted }) | |
| const sectionLabel = { fontSize: 9.5, fontWeight: 700, color: T.faint, letterSpacing: ".09em", marginBottom: 8, paddingInlineStart: 4 } | |
| const chatRow = (active) => ({ display: "flex", alignItems: "center", gap: 8, padding: "8px 8px 8px 12px", borderRadius: 9, cursor: "pointer", fontSize: 12.5, userSelect: "none", background: active ? "#f0fdf4" : "transparent", color: active ? T.green : T.muted, fontWeight: active ? 600 : 400 }) | |
| const dotsBtn = { background: "transparent", border: "none", color: T.faint, cursor: "pointer", padding: 2, borderRadius: 6, flexShrink: 0, display: "flex" } | |
| const userRow = { display: "flex", alignItems: "center", gap: 9, padding: "9px 8px", marginTop: 2 } | |
| const avatar = { width: 32, height: 32, borderRadius: "50%", background: T.gradient, color: "#fff", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 14, fontWeight: 700, flexShrink: 0 } | |
| const logoutBtn = { background: "transparent", border: "none", color: T.faint, cursor: "pointer", padding: 6, borderRadius: 8, display: "flex" } | |
| const menuBox = { background: "#fff", border: `1px solid ${T.border}`, borderRadius: 10, boxShadow: T.shadowMd, zIndex: 1700, overflow: "hidden", minWidth: 150 } | |
| const menuItem = { display: "flex", alignItems: "center", gap: 8, padding: "10px 14px", cursor: "pointer", color: T.danger, fontSize: 13, fontWeight: 600 } | |
| export default Sidebar | |