ui changes

This commit is contained in:
2026-08-10 19:36:45 +05:30
parent 409dffe938
commit 5cd49a1655
49 changed files with 7988 additions and 470 deletions

357
src/layouts/AdminLayout.jsx Normal file
View File

@@ -0,0 +1,357 @@
import React, { useEffect, useRef, useState } from 'react';
import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion';
import {
Bell, ChevronDown, LogOut, Menu, Search, Shield, User as UserIcon, Users,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { base44 } from '@/api/base44Client';
import { KROW_LOGO_URL } from '@/assets/brand';
import { Avatar } from '@/components/ds/Avatar';
import { Badge } from '@/components/ui/badge';
import {
DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
import { AssistantPanel, AssistantPanelProvider } from '@/components/ai-assistant';
import { endAdminSession } from '@/pages/admin/session';
/**
* AdminLayout — the shell for the Admin product.
*
* Built with the new operations console design system: single 56px header bar,
* compact search with keyboard shortcut, dedicated design tokens, and fluid layout.
*/
const ADMIN_ROOT = '/admin';
/**
* The visible Admin navigation — Admin routes only.
*
* Employer and Talent destinations are deliberately absent, KROW Forge
* (/university) included: it belongs to the Talent product, and linking out of an
* operations console into another product's navigation loses your place rather
* than saving you a step. Their code and their own entry points are untouched.
*/
const NAV = [
{ label: 'Control Center', path: ADMIN_ROOT },
{ label: 'Positions', path: '/admin/positions' },
{ label: 'Candidates', path: '/admin/candidates' },
{ label: 'Hired History', path: '/admin/hired' },
{ label: 'Talent Pool', path: '/admin/talent-pool' },
{ label: 'KROW Forge', path: '/admin/university' },
{ label: 'Analytics', path: '/admin/analytics' },
{ label: 'Activity', path: '/admin/activity' },
];
function GlobalSearch() {
const navigate = useNavigate();
const [query, setQuery] = useState('');
const [focused, setFocused] = useState(false);
const inputRef = useRef(null);
/* Cmd/Ctrl+K focuses search — jump-to control shortcut */
useEffect(() => {
const onKey = (e) => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
e.preventDefault();
inputRef.current?.focus();
}
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, []);
return (
<form
onSubmit={(e) => {
e.preventDefault();
if (query.trim()) navigate(`/admin/candidates?search=${encodeURIComponent(query.trim())}`);
}}
role="search"
className={cn(
'relative hidden items-center transition-all duration-base ease-out md:flex',
focused || query ? 'w-64' : 'w-44'
)}
>
<Search className="pointer-events-none absolute left-2.5 h-3.5 w-3.5 text-ink-4" aria-hidden="true" />
<input
ref={inputRef}
value={query}
onChange={(e) => setQuery(e.target.value)}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
placeholder="Search"
aria-label="Search the platform"
className="h-8 w-full rounded-lg border border-border bg-surface-subtle pl-8 pr-10 text-body-sm text-ink-1
placeholder:text-ink-4 outline-none transition-colors
focus:border-krow-blue focus:bg-surface focus:ring-2 focus:ring-krow-blue/15"
/>
{!focused && !query && (
<kbd className="pointer-events-none absolute right-2 rounded border border-border bg-surface px-1 text-[10px] font-medium text-ink-4">
⌘K
</kbd>
)}
</form>
);
}
function NotificationsMenu({ items }) {
const unread = items.filter((n) => !n.read).length;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
aria-label={unread ? `Notifications, ${unread} unread` : 'Notifications'}
className="relative grid h-8 w-8 place-items-center rounded-lg text-ink-3 transition-colors
hover:bg-surface-sunken hover:text-ink-1
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-krow-blue/50"
>
<Bell className="h-4 w-4" aria-hidden="true" />
{unread > 0 && (
<span className="absolute right-1 top-1 h-2 w-2 rounded-full bg-krow-blue ring-2 ring-white" />
)}
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-80 p-0">
<div className="flex items-center justify-between border-b border-border px-3 py-2.5">
<p className="text-body-sm font-semibold text-ink-1">Notifications</p>
{unread > 0 && <Badge variant="soft" size="sm">{unread} new</Badge>}
</div>
<div className="max-h-80 overflow-y-auto">
{items.map((n) => (
<div
key={n.id}
className={cn(
'flex gap-2.5 border-b border-border px-3 py-2.5 last:border-0',
!n.read && 'bg-krow-blue-tint/40'
)}
>
<span
className={cn(
'mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full',
n.severity === 'critical' ? 'bg-destructive'
: n.severity === 'warning' ? 'bg-warning' : 'bg-krow-blue'
)}
aria-hidden="true"
/>
<div className="min-w-0">
<p className="text-body-sm font-medium text-ink-1">{n.title}</p>
<p className="mt-0.5 text-caption leading-relaxed text-ink-3">{n.detail}</p>
<p className="mt-1 text-[10px] text-ink-4">{n.time}</p>
</div>
</div>
))}
</div>
</DropdownMenuContent>
</DropdownMenu>
);
}
export default function AdminLayout() {
const location = useLocation();
const navigate = useNavigate();
const [user, setUser] = useState(null);
const [menuOpen, setMenuOpen] = useState(false);
useEffect(() => {
base44.auth.me().then(setUser).catch(() => {});
}, []);
const signOut = () => {
endAdminSession();
base44.auth.logout('/admin/login');
};
const isActive = (path) => {
if (path === ADMIN_ROOT) return location.pathname === ADMIN_ROOT || location.pathname === '/overview';
const alias = path.replace('/admin', '');
return (
location.pathname === path ||
location.pathname.startsWith(`${path}/`) ||
(Boolean(alias) && alias !== '/' && (location.pathname === alias || location.pathname.startsWith(`${alias}/`)))
);
};
return (
<AssistantPanelProvider role="admin" pathname={location.pathname}>
<div className="min-h-screen bg-surface-subtle">
{/* Single bar header in the new Admin design system */}
<header className="sticky top-0 z-40 border-b border-border bg-white/85 backdrop-blur-xl">
<div className="flex h-14 items-center justify-between gap-4 px-4 lg:px-6">
<Link
to={ADMIN_ROOT}
aria-label="KROW Admin — Control Center"
className="flex shrink-0 items-center rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-krow-blue/50"
>
<img src={KROW_LOGO_URL} alt="KROW" className="h-6 w-auto object-contain" />
</Link>
{/* Text tabs with a hairline indicator, not filled pills. Eight
destinations means eight competing shapes if each one is a pill,
and a console header should recede — the underline states position
using the brand blue without turning the chrome into the loudest
thing on the page. */}
<nav aria-label="Admin" className="hidden min-w-0 flex-1 items-center gap-0.5 lg:flex">
{NAV.map((item) => (
<Link
key={item.path}
to={item.path}
aria-current={isActive(item.path) ? 'page' : undefined}
className={cn(
'relative whitespace-nowrap rounded-lg px-2.5 py-1.5 text-body-sm font-medium transition-colors duration-base',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-krow-blue/50',
isActive(item.path) ? 'text-krow-blue' : 'text-ink-3 hover:text-ink-1'
)}
>
{item.label}
{isActive(item.path) && (
<motion.span
layoutId="adminNavIndicator"
className="absolute inset-x-2.5 -bottom-[13px] h-0.5 rounded-full bg-krow-blue"
transition={{ type: 'spring', stiffness: 400, damping: 32 }}
/>
)}
</Link>
))}
</nav>
<div className="ml-auto flex items-center gap-1.5">
<GlobalSearch />
<NotificationsMenu items={NOTIFICATIONS} />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="flex items-center gap-1.5 rounded-lg py-1 pl-1 pr-1.5 transition-colors hover:bg-surface-sunken
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-krow-blue/50 cursor-pointer"
>
<Avatar name={user?.full_name || 'Admin'} size="sm" />
<ChevronDown className="h-3 w-3 text-ink-4" aria-hidden="true" />
<span className="sr-only">Account menu</span>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-60">
<DropdownMenuLabel className="font-normal">
<p className="text-body-sm font-semibold text-ink-1">{user?.full_name || 'Admin'}</p>
<p className="text-caption text-ink-3">{user?.email}</p>
<Badge variant="soft" size="sm" className="mt-1.5">Platform Administrator</Badge>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => navigate('/admin/profile')} className="cursor-pointer">
<UserIcon className="mr-2 h-4 w-4" /> Profile
</DropdownMenuItem>
<DropdownMenuItem onClick={() => navigate('/admin/profile#security')} className="cursor-pointer">
<Shield className="mr-2 h-4 w-4" /> Security
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={signOut}
className="cursor-pointer text-destructive focus:text-destructive"
>
<LogOut className="mr-2 h-4 w-4" /> Sign out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<button
type="button"
onClick={() => setMenuOpen(true)}
aria-label="Open navigation"
className="grid h-8 w-8 place-items-center rounded-lg text-ink-3 hover:bg-surface-sunken hover:text-ink-1 lg:hidden
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-krow-blue/50"
>
<Menu className="h-4 w-4" aria-hidden="true" />
</button>
</div>
</div>
</header>
{/* Mobile menu Sheet */}
<Sheet open={menuOpen} onOpenChange={setMenuOpen}>
<SheetContent side="right" className="w-72 p-0">
<SheetHeader className="border-b border-border px-4 py-4">
<SheetTitle className="text-left font-heading text-title">Admin</SheetTitle>
</SheetHeader>
<nav className="flex flex-col p-2">
{NAV.map((item) => (
<Link
key={item.path}
to={item.path}
onClick={() => setMenuOpen(false)}
className={cn(
'rounded-lg px-3 py-2.5 text-body-sm font-medium transition-colors',
isActive(item.path) ? 'bg-krow-blue-tint text-krow-blue' : 'text-ink-2 hover:bg-surface-sunken'
)}
>
{item.label}
</Link>
))}
<div className="my-2 h-px bg-border" />
<Link
to="/admin/profile"
onClick={() => setMenuOpen(false)}
className="rounded-lg px-3 py-2.5 text-body-sm font-medium text-ink-2 hover:bg-surface-sunken"
>
Profile
</Link>
</nav>
</SheetContent>
</Sheet>
{/* Main layout body */}
<div className="flex flex-col px-4 md:flex-row lg:px-6">
<main className="min-w-0 flex-1 py-6">
<Outlet />
</main>
<AssistantPanel
stickyClassName="top-14 pt-6"
panelHeightClassName="h-[calc(100dvh_-_6.5rem)]"
/>
</div>
</div>
</AssistantPanelProvider>
);
}
const NOTIFICATIONS = [
{
id: 1,
severity: 'warning',
title: 'Banquet Captain has no applicants',
detail: 'Live for 21 days with zero applications. Pay band may be below market.',
time: '2 hours ago',
read: false,
},
{
id: 2,
severity: 'warning',
title: '8 applicants awaiting screening',
detail: 'Oldest has been waiting 5 days across Event Security and Picker.',
time: '4 hours ago',
read: false,
},
{
id: 3,
severity: 'info',
title: 'Sofia Mendez scored 89',
detail: 'Screened for Event Server – Fine Dining and ready for a decision.',
time: 'Yesterday',
read: true,
},
{
id: 4,
severity: 'info',
title: '3 hires completed onboarding checks',
detail: 'Chef Antoine Dubois, Marcus Williams and Marco Rivera.',
time: '2 days ago',
read: true,
},
];