update ui design for all pages
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { ArrowRight, MapPin, Plus, Sparkles, Wallet } from 'lucide-react';
|
||||
import {
|
||||
ArrowRight, CheckCircle2, MapPin, MessageSquare, Sparkles, Star, Users, Wallet,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
Avatar, Badge, Button, Drawer, EmptyState, ProgressBar, SearchInput,
|
||||
@@ -73,35 +75,106 @@ function HealthChip({ health, className }) {
|
||||
);
|
||||
}
|
||||
|
||||
const STEP_ICONS = {
|
||||
applied: Users,
|
||||
screened: Sparkles,
|
||||
shortlisted: Star,
|
||||
interviewed: MessageSquare,
|
||||
hired: CheckCircle2,
|
||||
};
|
||||
|
||||
/**
|
||||
* The hiring progression: Applied → Screened → Shortlisted → Interviewed → Hired.
|
||||
*
|
||||
* Five labelled counts, each with a proportional bar, rather than one generic
|
||||
* progress bar. A single bar would have to pick one number to stand for the whole
|
||||
* funnel, and the interesting information is precisely *where* the funnel narrows.
|
||||
* Rendered as an executive multi-segment funnel bar and 5 stage micro-cards with tailored icons.
|
||||
*/
|
||||
function Progression({ steps, className }) {
|
||||
const entered = steps[0]?.count || 0;
|
||||
|
||||
return (
|
||||
<div className={cn('grid grid-cols-5 gap-1.5', className)} role="group" aria-label="Hiring progression">
|
||||
{steps.map((step) => {
|
||||
const share = entered ? (step.count / entered) * 100 : 0;
|
||||
return (
|
||||
<div key={step.key} className="min-w-0">
|
||||
<div className="h-1 overflow-hidden rounded-full bg-surface-sunken">
|
||||
<div
|
||||
className={cn('h-full rounded-full', step.count ? 'bg-krow-blue' : 'bg-transparent')}
|
||||
style={{ width: `${Math.max(share, step.count ? 8 : 0)}%` }}
|
||||
/>
|
||||
<div className={cn('w-full space-y-2', className)} role="group" aria-label="Hiring progression">
|
||||
{/* Top Proportional Funnel Progress Bar */}
|
||||
<div className="flex h-1.5 w-full overflow-hidden rounded-full bg-surface-sunken p-0.5 gap-0.5 border border-border/40">
|
||||
{steps.map((step) => {
|
||||
const share = entered ? (step.count / entered) * 100 : 0;
|
||||
if (!step.count) return null;
|
||||
|
||||
const colorMap = {
|
||||
applied: 'bg-blue-600',
|
||||
screened: 'bg-indigo-500',
|
||||
shortlisted: 'bg-violet-500',
|
||||
interviewed: 'bg-cyan-500',
|
||||
hired: 'bg-emerald-500',
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={step.key}
|
||||
className={cn('h-full rounded-full transition-all duration-500', colorMap[step.key] || 'bg-blue-600')}
|
||||
style={{ width: `${Math.max(share, 10)}%` }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 5 Executive Stage Micro-Cards */}
|
||||
<div className="grid grid-cols-5 gap-1.5">
|
||||
{steps.map((step) => {
|
||||
const hasCount = step.count > 0;
|
||||
const isHired = step.key === 'hired' && hasCount;
|
||||
const Icon = STEP_ICONS[step.key] || Users;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={step.key}
|
||||
className={cn(
|
||||
'group/step relative flex flex-col items-center justify-between rounded-xl p-1.5 border transition-all duration-200 text-center min-w-0',
|
||||
hasCount
|
||||
? isHired
|
||||
? 'bg-gradient-to-b from-emerald-50/90 to-emerald-100/40 border-emerald-300/80 shadow-2xs dark:from-emerald-950/40 dark:to-emerald-900/20 dark:border-emerald-800'
|
||||
: 'bg-gradient-to-b from-surface to-surface-subtle/80 border-border/70 shadow-2xs hover:border-blue-500/40 hover:shadow-xs'
|
||||
: 'bg-surface-subtle/30 border-border/40 text-ink-4'
|
||||
)}
|
||||
>
|
||||
{/* Icon & Count Badge */}
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon
|
||||
className={cn(
|
||||
'h-3 w-3 shrink-0',
|
||||
hasCount
|
||||
? isHired
|
||||
? 'text-emerald-600 dark:text-emerald-400'
|
||||
: 'text-blue-600 dark:text-blue-400'
|
||||
: 'text-ink-4/40'
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
'font-heading text-body-sm font-extrabold tabular-nums leading-none',
|
||||
hasCount
|
||||
? isHired
|
||||
? 'text-emerald-700 dark:text-emerald-400'
|
||||
: 'text-ink-1'
|
||||
: 'text-ink-4'
|
||||
)}
|
||||
>
|
||||
{step.count}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Stage Micro Label */}
|
||||
<span
|
||||
className={cn(
|
||||
'mt-1.5 truncate text-[9.5px] font-semibold tracking-tight leading-none w-full text-center',
|
||||
hasCount ? 'text-ink-2' : 'text-ink-4/60'
|
||||
)}
|
||||
>
|
||||
{step.label === 'Shortlisted' ? 'Shortlist' : step.label === 'Interviewed' ? 'Interview' : step.label}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1.5 font-heading text-body-sm font-bold tabular-nums leading-none text-ink-1">
|
||||
{step.count}
|
||||
</p>
|
||||
<p className="truncate text-[10px] leading-tight text-ink-4">{step.label}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -363,6 +436,12 @@ function PositionDrawer({ position, onClose, onViewCandidates }) {
|
||||
|
||||
export default function AdminPositions() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
/* No create form on this page, in any form — no drawer, no modal, no route.
|
||||
Creating a position is a conversation with Owliver beside the list (see
|
||||
lib/skills/positionFlow.js); it writes through `useCreateJobPosting`, which
|
||||
invalidates the query this page reads, so a position created in the panel
|
||||
appears in the grid without this component knowing it happened. */
|
||||
const [searchParams] = useSearchParams();
|
||||
const { data: postings = [], isLoading } = useJobPostings();
|
||||
const { data: applications = [] } = useApplications();
|
||||
@@ -427,11 +506,6 @@ export default function AdminPositions() {
|
||||
<AdminPage
|
||||
title="Positions"
|
||||
subtitle="Manage open roles, hiring demand and position performance."
|
||||
actions={
|
||||
<Button size="sm" shape="rounded" onClick={() => navigate('/positions/new')}>
|
||||
<Plus aria-hidden="true" /> Create Position
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{/* Controls. Search gets a fixed, sensible measure — wide enough for a role
|
||||
title, not so wide it becomes the page's subject. */}
|
||||
@@ -478,12 +552,12 @@ export default function AdminPositions() {
|
||||
description={
|
||||
isFiltered
|
||||
? 'Try a broader search, or clear the filters to see every role.'
|
||||
: 'Create a position and applications will start appearing here.'
|
||||
: 'Ask Owliver to create a position and applications will start appearing here.'
|
||||
}
|
||||
action={
|
||||
isFiltered
|
||||
? <Button variant="outline" size="sm" onClick={clearFilters}>Clear filters</Button>
|
||||
: <Button size="sm" onClick={() => navigate('/positions/new')}>Create Position</Button>
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user