production-dashboard #1

Merged
iqbal merged 13 commits from production-dashboard into main 2026-10-27 14:58:53 +00:00
4 changed files with 62 additions and 7 deletions
Showing only changes of commit 6d4850f4ce - Show all commits

View File

@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from 'react'
import { useClerk, useOrganization, useUser } from '@clerk/react'
import { useClerk, useOrganization, useOrganizationList, useUser } from '@clerk/react'
import { useQuery } from '@tanstack/react-query'
import { NavLink, Outlet, useLocation } from 'react-router-dom'
import { LANGS, locale, setLang, t, useLang, type Key } from '../i18n'
@@ -70,11 +70,57 @@ function CreditBalance() {
)
}
// The Learning space card switches the active organization. AuthGate keys the pages by orgId and
// clears the query cache, so Brain, schedules, results and credits all reload for the new one.
function OrgSwitcher() {
const [open, setOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)
const { organization } = useOrganization()
const { setActive, userMemberships } = useOrganizationList({ userMemberships: { infinite: true } })
useEffect(() => {
if (!open) return
const onClick = (e: MouseEvent) => { if (!ref.current?.contains(e.target as Node)) setOpen(false) }
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false) }
document.addEventListener('mousedown', onClick)
document.addEventListener('keydown', onKey)
return () => { document.removeEventListener('mousedown', onClick); document.removeEventListener('keydown', onKey) }
}, [open])
const choose = (id: string) => {
setOpen(false)
if (id !== organization?.id) void setActive?.({ organization: id })
}
return (
<div className="workspace-switch" ref={ref}>
<button type="button" className="workspace" aria-haspopup="listbox" aria-expanded={open}
title={t('layout.switchOrg')} onClick={() => setOpen(o => !o)}>
<span className="workspace-icon">{(organization?.name || 'L').charAt(0).toUpperCase()}</span>
<div>{t('layout.learningSpace')}<small>{organization?.name ?? '-'}</small></div>
<Icon name="chevron-updown" />
</button>
{open && (
<ul className="workspace-menu" role="listbox" aria-label={t('layout.switchOrg')}>
{userMemberships.data?.map(({ organization: org }) => (
<li key={org.id} role="option" aria-selected={org.id === organization?.id}>
<button type="button" onClick={() => choose(org.id)}>
<span className="workspace-icon">{org.name.charAt(0).toUpperCase()}</span>
<span>{org.name}</span>
{org.id === organization?.id && <Icon name="check" />}
</button>
</li>
))}
</ul>
)}
</div>
)
}
export function AppLayout() {
useLang()
const [help, setHelp] = useState(false)
const { user } = useUser()
const { organization } = useOrganization()
const { signOut } = useClerk()
const { pathname } = useLocation()
const current = NAV.find(n => pathname.startsWith(n.to))
@@ -84,10 +130,7 @@ export function AppLayout() {
<div className="app">
<aside className="sidebar">
<NavLink to="/brain"><Brand /></NavLink>
<div className="workspace">
<span className="workspace-icon">{(organization?.name || 'L').charAt(0).toUpperCase()}</span>
<div>{t('layout.learningSpace')}<small>{organization?.name ?? '-'}</small></div>
</div>
<OrgSwitcher />
<div className="nav-label">{t('nav.workspace')}</div>
<nav aria-label={t('layout.mainNav')}>
{NAV.map(n => (

View File

@@ -16,7 +16,8 @@ const paths: Record<string, string> = {
search: '<circle cx="10" cy="10" r="6"/><path d="m15 15 5 5"/>',
info: '<circle cx="12" cy="12" r="9"/><path d="M12 11v6M12 7v.2"/>',
check: '<path d="m5 12 4 4L19 6"/>',
logout: '<path d="M10 4H4v16h6M10 12h11m-4-4 4 4-4 4"/>',
'chevron-updown': '<path d="m8 9 4-4 4 4M8 15l4 4 4-4"/>',
logout:'<path d="M10 4H4v16h6M10 12h11m-4-4 4 4-4 4"/>',
plus: '<path d="M12 4v16M4 12h16"/>',
trash: '<path d="M3 6h18M9 3h6l1 3M6 6l1 15h10l1-15M10 10v7M14 10v7"/>',
video: '<rect x="3" y="5" width="18" height="14" rx="3"/><path d="m10 9 5 3-5 3z"/>',

View File

@@ -13,6 +13,7 @@ const en = {
'nav.results': 'Results',
'nav.workspace': 'WORKSPACE',
'layout.learningSpace': 'Learning space',
'layout.switchOrg': 'Switch organization',
'layout.breadcrumb': 'Workspace',
'layout.signOut': 'Sign out',
'layout.engine': 'learning-lab engine',
@@ -395,6 +396,7 @@ const id: Record<Key, string> = {
'nav.results': 'Hasil',
'nav.workspace': 'RUANG KERJA',
'layout.learningSpace': 'Ruang belajar',
'layout.switchOrg': 'Ganti organisasi',
'layout.breadcrumb': 'Ruang kerja',
'layout.signOut': 'Keluar',
'layout.engine': 'Engine learning-lab',

View File

@@ -54,6 +54,15 @@ input::placeholder,textarea::placeholder{color:#98a2b3}
.sidebar .brand-mark{width:38px;height:38px;border-radius:12px}
.workspace{border:1px solid var(--line);background:#f8faff;border-radius:14px;padding:10px;margin-bottom:26px;font-size:13.5px}
.workspace small{font-size:12px}
.workspace-switch{position:relative;margin-bottom:26px}
button.workspace{width:100%;margin:0;text-align:left;font:inherit;font-size:13.5px;color:inherit;cursor:pointer;transition:border-color .15s,background .15s}
button.workspace:hover,button.workspace[aria-expanded=true]{border-color:var(--soft);background:#f2f7ff}
.workspace-menu{position:absolute;z-index:6;top:calc(100% + 6px);left:0;right:0;max-height:280px;overflow:auto;margin:0;padding:6px;list-style:none;background:#fff;border:1px solid var(--line);border-radius:14px;box-shadow:0 16px 32px -12px rgba(16,24,40,.22)}
.workspace-menu button{display:flex;align-items:center;gap:10px;width:100%;padding:8px;border:0;border-radius:10px;background:none;font:inherit;font-size:13.5px;font-weight:550;color:var(--ink);text-align:left;cursor:pointer}
.workspace-menu button:hover{background:#f2f7ff}
.workspace-menu button>span:nth-child(2){flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.workspace-menu .workspace-icon{display:flex;align-items:center;justify-content:center;flex-shrink:0}
.workspace-menu svg{width:16px;height:16px;color:var(--blue)}
.workspace-icon{background:var(--light);color:var(--blue-dark);border-radius:9px;width:30px;height:30px;font-weight:800}
.nav-label{color:var(--muted);font-size:11px;letter-spacing:.08em;text-transform:uppercase;margin:0 12px 10px}
.sidebar nav{gap:4px}