Observer platform: mobile fixes, data/UX overhaul, animated nav
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m41s

- Fix dashboard default round selection to target active round instead of R1
- Move edition selector from dashboard header to hamburger menu via shared context
- Add observer-friendly status labels (Not Reviewed / Under Review / Reviewed)
- Fix pipeline completion: closed rounds show 100%, cap all rates at 100%
- Round badge on projects list shows furthest round reached
- Hide scores/evals for projects with zero evaluations
- Enhance project detail round history with pass/reject indicators from ProjectRoundState
- Remove irrelevant fields (Org Type, Budget, Duration) from project detail
- Clickable juror workload with expandable project assignments
- Humanize activity feed with icons and readable messages
- Fix jurors table: responsive card layout on mobile
- Fix criteria chart: horizontal bars for readable labels on mobile
- Animate hamburger menu open/close with CSS grid transition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 22:45:56 +01:00
parent 5eea430ebd
commit 213efdba87
11 changed files with 576 additions and 313 deletions

View File

@@ -41,13 +41,15 @@ type RoleNavProps = {
basePath: string
/** Optional status badge displayed next to the logo (e.g., remaining evaluations count) */
statusBadge?: React.ReactNode
/** Optional slot rendered in the mobile hamburger menu (between nav links and sign out) and desktop header */
editionSelector?: React.ReactNode
}
function isNavItemActive(pathname: string, href: string, basePath: string): boolean {
return pathname === href || (href !== basePath && pathname.startsWith(href))
}
export function RoleNav({ navigation, roleName, user, basePath, statusBadge }: RoleNavProps) {
export function RoleNav({ navigation, roleName, user, basePath, statusBadge, editionSelector }: RoleNavProps) {
const pathname = usePathname()
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
const { status: sessionStatus } = useSession()
@@ -93,6 +95,7 @@ export function RoleNav({ navigation, roleName, user, basePath, statusBadge }: R
{/* User menu & mobile toggle */}
<div className="flex items-center gap-2">
{editionSelector && <div className="hidden md:block">{editionSelector}</div>}
{mounted && (
<Button
variant="ghost"
@@ -161,42 +164,54 @@ export function RoleNav({ navigation, roleName, user, basePath, statusBadge }: R
</div>
</div>
{/* Mobile menu */}
{isMobileMenuOpen && (
<div className="border-t md:hidden">
<nav className="container-app py-4 space-y-1">
{navigation.map((item) => {
const isActive = isNavItemActive(pathname, item.href, basePath)
return (
<Link
key={item.name}
href={item.href as Route}
onClick={() => setIsMobileMenuOpen(false)}
className={cn(
'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors',
isActive
? 'bg-primary/10 text-primary'
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
)}
{/* Mobile menu — animated with CSS grid */}
<div
className={cn(
'grid md:hidden transition-[grid-template-rows] duration-200 ease-out',
isMobileMenuOpen ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]',
)}
>
<div className="overflow-hidden">
<div className={cn('border-t', !isMobileMenuOpen && 'border-transparent')}>
<nav className="container-app py-4 space-y-1">
{navigation.map((item) => {
const isActive = isNavItemActive(pathname, item.href, basePath)
return (
<Link
key={item.name}
href={item.href as Route}
onClick={() => setIsMobileMenuOpen(false)}
className={cn(
'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors',
isActive
? 'bg-primary/10 text-primary'
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
)}
>
<item.icon className="h-4 w-4" />
{item.name}
</Link>
)
})}
{editionSelector && (
<div className="border-t pt-4 mt-4 px-3">
{editionSelector}
</div>
)}
<div className="border-t pt-4 mt-4">
<Button
variant="ghost"
className="w-full justify-start text-destructive hover:text-destructive"
onClick={() => signOut({ callbackUrl: '/login' })}
>
<item.icon className="h-4 w-4" />
{item.name}
</Link>
)
})}
<div className="border-t pt-4 mt-4">
<Button
variant="ghost"
className="w-full justify-start text-destructive hover:text-destructive"
onClick={() => signOut({ callbackUrl: '/login' })}
>
<LogOut className="mr-2 h-4 w-4" />
Sign Out
</Button>
</div>
</nav>
<LogOut className="mr-2 h-4 w-4" />
Sign Out
</Button>
</div>
</nav>
</div>
</div>
)}
</div>
</header>
)
}