update ui design for all pages

This commit is contained in:
2026-08-12 18:56:56 +05:30
parent 25c4ee4a6a
commit 3beb66059b
58 changed files with 7892 additions and 1891 deletions

View File

@@ -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
}
/>
)}