layout similar

This commit is contained in:
DaKheera47
2026-01-15 12:50:32 +00:00
parent f7347a180d
commit 110956ea46
3 changed files with 502 additions and 274 deletions
+207 -273
View File
@@ -19,13 +19,23 @@ import {
Sparkles,
X,
} from "lucide-react";
import { Link } from "react-router-dom";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import {
PageHeader,
StatusIndicator,
ListItem,
EmptyState,
ScoreMeter,
SplitLayout,
ListPanel,
DetailPanel,
PageMain,
} from "../components";
import * as api from "../api";
import type {
VisaSponsor,
@@ -53,44 +63,14 @@ const formatDateTime = (dateStr?: string | null) => {
}
};
/**
* Get score styling based on match quality
*/
const getScoreTokens = (score: number) => {
if (score >= 90)
return {
badge: "border-emerald-500/30 bg-emerald-500/10 text-emerald-200",
bar: "bg-emerald-500/80",
};
return { badge: "border-emerald-500/30 bg-emerald-500/10 text-emerald-200" };
if (score >= 70)
return {
badge: "border-amber-500/30 bg-amber-500/10 text-amber-200",
bar: "bg-amber-500/80",
};
return { badge: "border-amber-500/30 bg-amber-500/10 text-amber-200" };
if (score >= 50)
return {
badge: "border-orange-500/30 bg-orange-500/10 text-orange-200",
bar: "bg-orange-500/80",
};
return {
badge: "border-rose-500/30 bg-rose-500/10 text-rose-200",
bar: "bg-rose-500/80",
};
};
const ScoreMeter: React.FC<{ score: number }> = ({ score }) => {
const tokens = getScoreTokens(score);
return (
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<div className="h-1.5 w-12 rounded-full bg-muted/40">
<div
className={cn("h-1.5 rounded-full", tokens.bar)}
style={{ width: `${Math.max(4, Math.min(100, score))}%` }}
/>
</div>
<span className="tabular-nums text-foreground">{score}%</span>
</div>
);
return { badge: "border-orange-500/30 bg-orange-500/10 text-orange-200" };
return { badge: "border-rose-500/30 bg-rose-500/10 text-rose-200" };
};
export const VisaSponsorsPage: React.FC = () => {
@@ -195,7 +175,6 @@ export const VisaSponsorsPage: React.FC = () => {
const result = await api.updateVisaSponsorList();
setStatus(result.status);
toast.success(result.message);
// Re-run search if there was a query
if (searchQuery.trim()) {
handleSearch(searchQuery);
}
@@ -212,29 +191,18 @@ export const VisaSponsorsPage: React.FC = () => {
[results, selectedOrg]
);
const isUpdateInProgress = isUpdating || status?.isUpdating;
return (
<>
{/* Header */}
<header className="sticky top-0 z-40 border-b bg-background/80 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container mx-auto flex max-w-7xl items-center justify-between gap-4 px-4 py-4">
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-lg border border-border/60 bg-muted/30">
<Shield className="h-4 w-4 text-muted-foreground" />
</div>
<div className="leading-tight">
<div className="text-sm font-semibold tracking-tight">Visa Sponsors</div>
<div className="text-xs text-muted-foreground">UK Register Search</div>
</div>
{(isUpdating || status?.isUpdating) && (
<span className="inline-flex items-center gap-2 rounded-full border border-amber-500/30 bg-amber-500/10 px-2 py-1 text-[11px] font-semibold uppercase tracking-wide text-amber-200">
<span className="h-1.5 w-1.5 rounded-full bg-amber-400 animate-pulse" />
Updating
</span>
)}
</div>
<div className="flex items-center gap-2">
{/* Status info */}
<PageHeader
icon={Shield}
title="Visa Sponsors"
subtitle="UK Register Search"
statusIndicator={isUpdateInProgress ? <StatusIndicator label="Updating" /> : undefined}
nav={[{ icon: Sparkles, label: "Back to Orchestrator", to: "/" }]}
actions={
<>
{status && (
<div className="hidden md:flex items-center gap-4 text-xs text-muted-foreground mr-2">
<span className="flex items-center gap-1.5">
@@ -247,43 +215,37 @@ export const VisaSponsorsPage: React.FC = () => {
</span>
</div>
)}
<Button
variant="ghost"
size="icon"
onClick={handleUpdate}
disabled={isUpdating || status?.isUpdating}
disabled={isUpdateInProgress}
aria-label="Update sponsor list"
>
{isUpdating || status?.isUpdating ? (
{isUpdateInProgress ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Download className="h-4 w-4" />
)}
</Button>
</>
}
/>
<Button asChild variant="ghost" size="icon" aria-label="Back to Orchestrator">
<Link to="/">
<Sparkles className="h-4 w-4" />
</Link>
</Button>
</div>
</div>
</header>
{/* Main content */}
<div className="flex flex-1 overflow-hidden">
{/* Left panel - Search and results */}
<div className="flex w-[420px] flex-col border-r">
{/* Search input */}
<div className="border-b p-4">
<PageMain>
{/* Search section */}
<section className="rounded-xl border border-border/60 bg-card/40 p-4">
<div className="space-y-2">
<label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Company name
</label>
<div className="relative">
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="Search for a company name..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 pr-10"
className="pl-10 pr-10 h-10"
autoFocus
/>
{searchQuery && (
@@ -295,214 +257,186 @@ export const VisaSponsorsPage: React.FC = () => {
</button>
)}
</div>
{isSearching && (
<div className="mt-2 flex items-center gap-2 text-xs text-muted-foreground">
<Loader2 className="h-3 w-3 animate-spin" />
Searching...
</div>
)}
<p className="text-xs text-muted-foreground">
Enter a company name to check if they're a licensed UK visa sponsor.
</p>
</div>
</section>
{/* Results list */}
<div className="flex-1 overflow-y-auto">
{/* No data state */}
{!isLoadingStatus && status?.totalSponsors === 0 && (
<div className="flex flex-col items-center justify-center h-full p-8 text-center">
<AlertCircle className="h-10 w-10 text-amber-400 mb-4" />
<div className="text-sm font-medium text-foreground mb-1">
No sponsor data available
</div>
<p className="text-xs text-muted-foreground mb-4 max-w-xs">
The visa sponsor list hasn't been downloaded yet.
</p>
<Button size="sm" onClick={handleUpdate} disabled={isUpdating}>
{isUpdating ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Downloading...
</>
) : (
<>
<Download className="h-4 w-4 mr-2" />
Download List
</>
)}
</Button>
</div>
)}
{/* Empty search state */}
{status && status.totalSponsors > 0 && !searchQuery && (
<div className="flex flex-col items-center justify-center h-full p-8 text-center">
<Search className="h-10 w-10 text-muted-foreground/50 mb-4" />
<div className="text-sm font-medium text-foreground mb-1">
Search for a company
</div>
<p className="text-xs text-muted-foreground max-w-xs">
Enter a company name to check if they're on the UK visa sponsor register.
</p>
</div>
)}
{/* No results state */}
{searchQuery && !isSearching && results.length === 0 && (
<div className="flex flex-col items-center justify-center h-full p-8 text-center">
<AlertCircle className="h-10 w-10 text-muted-foreground/50 mb-4" />
<div className="text-sm font-medium text-foreground mb-1">
No matches found
</div>
<p className="text-xs text-muted-foreground max-w-xs">
No sponsors match "{searchQuery}". Try a different spelling.
</p>
</div>
)}
{/* Results */}
{results.length > 0 && (
<div className="divide-y divide-border/50">
{results.map((result, index) => (
<button
key={`${result.sponsor.organisationName}-${index}`}
onClick={() => fetchOrgDetails(result.sponsor.organisationName)}
className={cn(
"w-full px-4 py-3 text-left transition-colors",
selectedOrg === result.sponsor.organisationName
? "bg-muted/50"
: "hover:bg-muted/30"
)}
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<Building2 className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
<span className="text-sm font-medium text-foreground truncate">
{result.sponsor.organisationName}
</span>
</div>
{(result.sponsor.townCity || result.sponsor.county) && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<MapPin className="h-3 w-3" />
{[result.sponsor.townCity, result.sponsor.county]
.filter(Boolean)
.join(", ")}
</div>
)}
</div>
<div className="flex items-center gap-2 shrink-0">
<ScoreMeter score={result.score} />
<ChevronRight className="h-4 w-4 text-muted-foreground" />
</div>
</div>
</button>
))}
</div>
)}
</div>
{/* Results count footer */}
{results.length > 0 && (
<div className="border-t px-4 py-2 text-xs text-muted-foreground">
{results.length} result{results.length !== 1 ? "s" : ""}
</div>
)}
</div>
{/* Right panel - Details */}
<div className="flex-1 overflow-y-auto">
{!selectedOrg ? (
<div className="flex flex-col items-center justify-center h-full p-8 text-center">
<Building2 className="h-10 w-10 text-muted-foreground/50 mb-4" />
<div className="text-sm font-medium text-foreground mb-1">
Select a company
</div>
<p className="text-xs text-muted-foreground">
Click on a search result to view details.
</p>
</div>
) : isLoadingDetails ? (
<div className="flex items-center justify-center h-full">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : (
<div className="p-6">
{/* Header */}
<div className="mb-6">
<div className="flex items-center gap-2 mb-2">
<span className="inline-flex items-center gap-1.5 rounded-full border border-emerald-500/30 bg-emerald-500/10 px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-emerald-200">
<CheckCircle2 className="h-3 w-3" />
Licensed Sponsor
</span>
{selectedResult && (
<span
className={cn(
"inline-flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide",
getScoreTokens(selectedResult.score).badge
)}
>
{selectedResult.score}% Match
<SplitLayout>
{/* Left panel - Results */}
<ListPanel
footer={
results.length > 0 ? (
<div className="text-xs text-muted-foreground">
{results.length} result{results.length !== 1 ? "s" : ""}
{isSearching && (
<span className="ml-2">
<Loader2 className="inline h-3 w-3 animate-spin" />
</span>
)}
</div>
<h2 className="text-xl font-semibold text-foreground">
{selectedOrg}
</h2>
</div>
) : null
}
>
{!isLoadingStatus && status?.totalSponsors === 0 && (
<EmptyState
icon={AlertCircle}
title="No sponsor data available"
description="The visa sponsor list hasn't been downloaded yet."
action={
<Button size="sm" onClick={handleUpdate} disabled={isUpdating}>
{isUpdating ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Downloading...
</>
) : (
<>
<Download className="h-4 w-4 mr-2" />
Download List
</>
)}
</Button>
}
/>
)}
{/* Location */}
{orgDetails.length > 0 && (orgDetails[0].townCity || orgDetails[0].county) && (
<div className="mb-6">
<div className="text-xs font-medium uppercase tracking-wide text-muted-foreground mb-2">
Location
</div>
<div className="flex items-center gap-2 text-sm text-foreground">
<MapPin className="h-4 w-4 text-muted-foreground" />
{[orgDetails[0].townCity, orgDetails[0].county]
.filter(Boolean)
.join(", ")}
</div>
</div>
)}
{status && status.totalSponsors > 0 && !searchQuery && (
<EmptyState
icon={Search}
title="Search for a company"
description="Enter a company name above to check the sponsor register."
/>
)}
{/* Licence types / routes */}
<div className="mb-6">
<div className="text-xs font-medium uppercase tracking-wide text-muted-foreground mb-3">
Licensed Routes ({orgDetails.length})
</div>
<div className="space-y-2">
{orgDetails.map((entry, index) => (
<div
key={index}
className="rounded-lg border border-border/60 bg-muted/20 p-4"
>
<div className="flex items-start justify-between gap-2 mb-2">
<Badge variant="secondary" className="text-xs">
{entry.route}
</Badge>
</div>
<div className="text-xs text-muted-foreground">
<span className="font-medium text-foreground">Type & Rating:</span>{" "}
{entry.typeRating}
</div>
{searchQuery && !isSearching && results.length === 0 && (
<EmptyState
icon={AlertCircle}
title="No matches found"
description={`No sponsors match "${searchQuery}". Try a different spelling.`}
/>
)}
{results.length > 0 &&
results.map((result, index) => (
<ListItem
key={`${result.sponsor.organisationName}-${index}`}
selected={selectedOrg === result.sponsor.organisationName}
onClick={() => fetchOrgDetails(result.sponsor.organisationName)}
className="gap-3"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<Building2 className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
<span className="text-sm font-medium text-foreground truncate">
{result.sponsor.organisationName}
</span>
</div>
))}
</div>
</div>
{(result.sponsor.townCity || result.sponsor.county) && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<MapPin className="h-3 w-3" />
{[result.sponsor.townCity, result.sponsor.county]
.filter(Boolean)
.join(", ")}
</div>
)}
</div>
<div className="flex items-center gap-2 shrink-0">
<ScoreMeter score={result.score} />
<ChevronRight className="h-4 w-4 text-muted-foreground" />
</div>
</ListItem>
))}
</ListPanel>
{/* Info box */}
<div className="rounded-lg border border-sky-500/30 bg-sky-500/10 p-4 text-sm">
<div className="font-medium text-sky-200 mb-1">
What does this mean?
</div>
<p className="text-xs text-sky-300/80">
This organisation is licensed by the UK Home Office to sponsor workers
on the routes listed above. An "A rating" means they're fully compliant
with their sponsor duties.
{/* Right panel - Details */}
<DetailPanel>
{!selectedOrg ? (
<div className="flex h-full flex-col items-center justify-center gap-2 text-center">
<div className="text-base font-semibold">Select a company</div>
<p className="text-sm text-muted-foreground">
Pick a company from the results to see details here.
</p>
</div>
</div>
)}
</div>
</div>
) : isLoadingDetails ? (
<div className="flex items-center justify-center h-32">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : (
<div className="space-y-4">
{/* Header */}
<div>
<div className="flex items-center gap-2 mb-2">
<span className="inline-flex items-center gap-1.5 rounded-full border border-emerald-500/30 bg-emerald-500/10 px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-emerald-200">
<CheckCircle2 className="h-3 w-3" />
Licensed Sponsor
</span>
{selectedResult && (
<span
className={cn(
"inline-flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide",
getScoreTokens(selectedResult.score).badge
)}
>
{selectedResult.score}% Match
</span>
)}
</div>
<h2 className="text-lg font-semibold text-foreground">{selectedOrg}</h2>
</div>
{/* Location */}
{orgDetails.length > 0 && (orgDetails[0].townCity || orgDetails[0].county) && (
<div>
<div className="text-xs font-medium uppercase tracking-wide text-muted-foreground mb-1">
Location
</div>
<div className="flex items-center gap-2 text-sm text-foreground">
<MapPin className="h-4 w-4 text-muted-foreground" />
{[orgDetails[0].townCity, orgDetails[0].county].filter(Boolean).join(", ")}
</div>
</div>
)}
{/* Licence types / routes */}
<div>
<div className="text-xs font-medium uppercase tracking-wide text-muted-foreground mb-2">
Licensed Routes ({orgDetails.length})
</div>
<div className="space-y-2">
{orgDetails.map((entry, index) => (
<div
key={index}
className="rounded-lg border border-border/60 bg-muted/20 p-3"
>
<div className="flex items-start justify-between gap-2 mb-1">
<Badge variant="secondary" className="text-xs">
{entry.route}
</Badge>
</div>
<div className="text-xs text-muted-foreground">
<span className="font-medium text-foreground">Type & Rating:</span>{" "}
{entry.typeRating}
</div>
</div>
))}
</div>
</div>
{/* Info box */}
<div className="rounded-lg border border-sky-500/30 bg-sky-500/10 p-3 text-sm">
<div className="font-medium text-sky-200 mb-1">What does this mean?</div>
<p className="text-xs text-sky-300/80">
This organisation is licensed by the UK Home Office to sponsor workers on the
routes listed above. An "A rating" means they're fully compliant.
</p>
</div>
</div>
)}
</DetailPanel>
</SplitLayout>
</PageMain>
</>
);
};