Observer platform: mobile fixes, data/UX overhaul, animated nav
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m41s
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:
@@ -2,11 +2,40 @@
|
||||
|
||||
import { BarChart3, Home, FolderKanban } from 'lucide-react'
|
||||
import { RoleNav, type NavItem, type RoleNavUser } from '@/components/layouts/role-nav'
|
||||
import { useEditionContext } from '@/components/observer/observer-edition-context'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
|
||||
interface ObserverNavProps {
|
||||
user: RoleNavUser
|
||||
}
|
||||
|
||||
function EditionSelector() {
|
||||
const { programs, selectedProgramId, setSelectedProgramId } = useEditionContext()
|
||||
|
||||
if (programs.length <= 1) return null
|
||||
|
||||
return (
|
||||
<Select value={selectedProgramId} onValueChange={setSelectedProgramId}>
|
||||
<SelectTrigger className="w-full md:w-[180px]">
|
||||
<SelectValue placeholder="Select edition" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{programs.map((p) => (
|
||||
<SelectItem key={p.id} value={p.id}>
|
||||
{p.year ? `${p.year} Edition` : p.name ?? p.id}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
|
||||
export function ObserverNav({ user }: ObserverNavProps) {
|
||||
const navigation: NavItem[] = [
|
||||
{
|
||||
@@ -32,6 +61,7 @@ export function ObserverNav({ user }: ObserverNavProps) {
|
||||
roleName="Observer"
|
||||
user={user}
|
||||
basePath="/observer"
|
||||
editionSelector={<EditionSelector />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user