ac-tool/frontend/src/components/layout/Sidebar.tsx
Vadym Samoilenko 8f57c657fa Add Help page with full User Guide
Covers: overview, sheets, AI commands, brief extraction, export CSV,
export templates, admin (clients, dropdowns, users), and login/emergency
access. Admin-only sections are hidden for regular users.
Accessible via sidebar "Help" link at /help.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 19:32:17 +00:00

278 lines
12 KiB
TypeScript

import { useState } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
import { useSheetStore } from '../../stores/useSheetStore'
import { useAuthStore } from '../../stores/useAuthStore'
// Inline SVGs — no extra deps
const IconFile = () => (
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
</svg>
)
const IconGrid = () => (
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
<rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>
</svg>
)
const IconSettings = () => (
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
)
const IconUpload = () => (
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
)
const IconPlus = () => (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
</svg>
)
const IconHelp = () => (
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"/>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
)
export default function Sidebar() {
const navigate = useNavigate()
const location = useLocation()
const { sheets, activeSheetId, loadSheet, createSheet, renameSheet, deleteSheet, duplicateSheet } = useSheetStore()
const user = useAuthStore(s => s.user)
const [renamingId, setRenamingId] = useState<string | null>(null)
const [renameValue, setRenameValue] = useState('')
const [contextMenu, setContextMenu] = useState<{ id: string; x: number; y: number } | null>(null)
const isActive = (path: string) => location.pathname === path || location.pathname.startsWith(path + '/')
const handleNewSheet = async () => {
const name = `Sheet ${new Date().toLocaleDateString()}`
const id = await createSheet(name)
navigate(`/sheet/${id}`)
}
const handleSelect = async (id: string) => {
await loadSheet(id)
navigate(`/sheet/${id}`)
}
const handleContextMenu = (e: React.MouseEvent, id: string) => {
e.preventDefault()
setContextMenu({ id, x: e.clientX, y: e.clientY })
}
const handleRename = (id: string, current: string) => {
setRenamingId(id)
setRenameValue(current)
setContextMenu(null)
}
const commitRename = async (id: string) => {
if (renameValue.trim()) await renameSheet(id, renameValue.trim())
setRenamingId(null)
}
return (
<div
className="flex flex-col w-60 flex-shrink-0 border-r overflow-hidden"
style={{ background: 'var(--bg-sidebar)', borderColor: 'var(--border)' }}
onClick={() => setContextMenu(null)}
>
{/* ── Logo ── */}
<div className="px-4 py-4 border-b" style={{ borderColor: 'var(--border)' }}>
<button
onClick={() => navigate('/')}
className="flex items-center gap-2.5 w-full text-left group"
style={{ background: 'transparent', border: 'none', padding: 0 }}
aria-label="Go to Dashboard"
>
<div
className="w-7 h-7 rounded-lg flex items-center justify-center font-black text-xs tracking-tight flex-shrink-0 group-hover:opacity-90 transition-opacity"
style={{ background: 'var(--accent)', color: '#000' }}
>
AC
</div>
<div>
<div className="font-bold text-sm leading-tight" style={{ color: 'var(--text-primary)' }}>AC Tool</div>
<div className="text-xs leading-none" style={{ color: 'var(--text-muted)' }}>Oliver Agency</div>
</div>
</button>
</div>
{/* ── Nav items ── */}
<div className="px-2 pt-3 pb-1 space-y-0.5">
<button
onClick={() => navigate('/')}
className="w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm text-left transition-colors"
style={{
background: isActive('/') && !location.pathname.startsWith('/sheet') && !location.pathname.startsWith('/brief') && !location.pathname.startsWith('/admin') ? 'var(--accent-dim)' : 'transparent',
color: isActive('/') && !location.pathname.startsWith('/sheet') && !location.pathname.startsWith('/brief') && !location.pathname.startsWith('/admin') ? 'var(--accent)' : 'var(--text-secondary)',
}}
>
<IconGrid />
Dashboard
</button>
<button
onClick={() => navigate('/brief/upload')}
className="w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm text-left transition-colors"
style={{
background: location.pathname.startsWith('/brief') ? 'var(--accent-dim)' : 'transparent',
color: location.pathname.startsWith('/brief') ? 'var(--accent)' : 'var(--text-secondary)',
}}
>
<IconUpload />
Upload Brief
</button>
<button
onClick={() => navigate('/help')}
className="w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm text-left transition-colors"
style={{
background: location.pathname === '/help' ? 'var(--accent-dim)' : 'transparent',
color: location.pathname === '/help' ? 'var(--accent)' : 'var(--text-secondary)',
}}
>
<IconHelp />
Help
</button>
</div>
{/* ── Divider ── */}
<div className="mx-4 my-2" style={{ height: 1, background: 'var(--border)' }} />
{/* ── Sheets list ── */}
<div className="flex-1 overflow-y-auto px-2 pb-2">
<div className="flex items-center justify-between px-3 py-1.5 mb-1">
<span className="text-xs font-semibold uppercase tracking-widest" style={{ color: 'var(--text-muted)' }}>
Sheets
</span>
<button
onClick={handleNewSheet}
className="w-6 h-6 rounded-md flex items-center justify-center transition-colors hover:opacity-80"
style={{ color: 'var(--accent)', background: 'var(--accent-dim)' }}
title="New Sheet"
>
<IconPlus />
</button>
</div>
{sheets.map(sheet => {
const active = activeSheetId === sheet.id
return (
<div
key={sheet.id}
onContextMenu={e => handleContextMenu(e, sheet.id)}
className="group flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer mb-0.5 transition-colors"
style={{
background: active ? 'var(--accent-dim)' : 'transparent',
borderLeft: active ? '2px solid var(--accent)' : '2px solid transparent',
}}
onClick={() => handleSelect(sheet.id)}
>
{renamingId === sheet.id ? (
<input
autoFocus
value={renameValue}
onChange={e => setRenameValue(e.target.value)}
onBlur={() => commitRename(sheet.id)}
onKeyDown={e => {
if (e.key === 'Enter') commitRename(sheet.id)
if (e.key === 'Escape') setRenamingId(null)
}}
className="flex-1 bg-transparent text-sm outline-none border-b"
style={{ color: 'var(--text-primary)', borderColor: 'var(--accent)' }}
onClick={e => e.stopPropagation()}
/>
) : (
<>
<span style={{ color: active ? 'var(--accent)' : 'var(--text-muted)', flexShrink: 0 }}>
<IconFile />
</span>
<div className="flex-1 min-w-0">
<div className="text-xs font-medium truncate" style={{ color: active ? 'var(--accent)' : 'var(--text-primary)' }}>
{sheet.name}
</div>
<div className="text-xs" style={{ color: 'var(--text-muted)' }}>{sheet.itemCount} items</div>
</div>
</>
)}
</div>
)
})}
{sheets.length === 0 && (
<div className="px-3 py-6 text-center" style={{ color: 'var(--text-muted)' }}>
<div className="text-2xl mb-2 opacity-30">
<IconFile />
</div>
<div className="text-xs">No sheets yet</div>
<div className="text-xs mt-0.5 opacity-70">Click + to create one</div>
</div>
)}
</div>
{/* ── Admin ── */}
{user?.role === 'admin' && (
<div className="px-2 py-2 border-t space-y-0.5" style={{ borderColor: 'var(--border)' }}>
{[
{ label: 'Users', path: '/admin/users' },
{ label: 'Clients', path: '/admin/clients' },
{ label: 'Dropdowns', path: '/admin/dropdowns' },
].map(item => (
<button
key={item.path}
onClick={() => navigate(item.path)}
className="w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm text-left transition-colors"
style={{
background: location.pathname === item.path ? 'var(--accent-dim)' : 'transparent',
color: location.pathname === item.path ? 'var(--accent)' : 'var(--text-muted)',
}}
>
<IconSettings />
{item.label}
</button>
))}
</div>
)}
{/* ── Context menu ── */}
{contextMenu && (
<div
className="fixed z-50 py-1 shadow-2xl"
style={{
top: contextMenu.y, left: contextMenu.x,
background: 'var(--bg-elevated)',
border: '1px solid var(--border-light)',
borderRadius: 'var(--radius-md)',
minWidth: 148,
}}
onClick={e => e.stopPropagation()}
>
{[
{ label: 'Rename', action: () => { const s = sheets.find(sh => sh.id === contextMenu.id); if (s) handleRename(s.id, s.name) } },
{ label: 'Duplicate', action: async () => { await duplicateSheet(contextMenu.id); setContextMenu(null) } },
{ label: 'Delete', action: async () => { if (confirm('Delete this sheet?')) { await deleteSheet(contextMenu.id); setContextMenu(null) } }, danger: true },
].map(item => (
<button
key={item.label}
onClick={item.action}
className="w-full text-left px-3 py-2 text-xs font-medium hover:opacity-70 transition-opacity"
style={{ color: item.danger ? 'var(--danger)' : 'var(--text-primary)', background: 'transparent', border: 'none' }}
>
{item.label}
</button>
))}
</div>
)}
</div>
)
}