diff --git a/orchestrator/package.json b/orchestrator/package.json index 225d693..15ce3c9 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -7,7 +7,7 @@ "scripts": { "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"", "dev:server": "tsx watch src/server/index.ts", - "dev:client": "vite", + "dev:client": "vite --host", "build": "npm run build:client && npm run build:server", "build:server": "tsc -p tsconfig.server.json", "build:client": "vite build", diff --git a/orchestrator/src/client/components/Header.tsx b/orchestrator/src/client/components/Header.tsx index 0365d6f..5c0faae 100644 --- a/orchestrator/src/client/components/Header.tsx +++ b/orchestrator/src/client/components/Header.tsx @@ -4,28 +4,19 @@ import React from "react"; import { + Briefcase, ChevronDown, + Home, Loader2, + Menu, Play, RefreshCcw, Settings, Shield, - Trash2, } from "lucide-react"; -import { Link } from "react-router-dom"; +import { Link, useLocation } from "react-router-dom"; import { Button } from "@/components/ui/button"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; import { DropdownMenu, DropdownMenuCheckboxItem, @@ -35,6 +26,13 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; import type { JobSource } from "../../shared/types"; interface HeaderProps { @@ -54,6 +52,9 @@ export const Header: React.FC = ({ pipelineSources, onPipelineSourcesChange, }) => { + const location = useLocation(); + const [sheetOpen, setSheetOpen] = React.useState(false); + const sourceLabel: Record = { gradcracker: "Gradcracker", indeed: "Indeed", @@ -63,6 +64,13 @@ export const Header: React.FC = ({ const orderedSources: JobSource[] = ["gradcracker", "indeed", "linkedin", "ukvisajobs"]; + const navLinks = [ + { to: "/", label: "Dashboard", icon: Home }, + { to: "/visa-sponsors", label: "Visa Sponsors", icon: Shield }, + { to: "/ukvisajobs", label: "UK Visa Jobs", icon: Briefcase }, + { to: "/settings", label: "Settings", icon: Settings }, + ]; + const toggleSource = (source: JobSource, checked: boolean) => { const next = checked ? Array.from(new Set([...pipelineSources, source])) @@ -75,22 +83,55 @@ export const Header: React.FC = ({ return (
- -
- Job Ops Logo -
-
-
Job Ops
-
Orchestrator
-
- +
+ + + + + + + Navigation + + + + + + +
+ Job Ops Logo +
+
+
Job Ops
+
Orchestrator
+
+ +
- - - -
- ))} - {actions} + return ( +
+
+
+ + + + + + + Navigation + + + + + +
+ +
+
+
{title}
+
{subtitle}
+
+ {badge && ( + + {badge} + + )} + {statusIndicator} +
+ +
+ {actions} +
-
-
-); + + ); +}; // ============================================================================ // Status Indicator (Pipeline running, Updating, etc.) diff --git a/orchestrator/src/client/pages/OrchestratorPage.tsx b/orchestrator/src/client/pages/OrchestratorPage.tsx index 590ce1e..24fe4e6 100644 --- a/orchestrator/src/client/pages/OrchestratorPage.tsx +++ b/orchestrator/src/client/pages/OrchestratorPage.tsx @@ -5,6 +5,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ArrowUpDown, + Briefcase, Calendar, CheckCircle2, ChevronDown, @@ -14,8 +15,10 @@ import { ExternalLink, FileText, Filter, + Home, Loader2, MapPin, + Menu, MoreHorizontal, Play, RefreshCcw, @@ -26,7 +29,7 @@ import { Sparkles, XCircle, } from "lucide-react"; -import { Link } from "react-router-dom"; +import { Link, useLocation } from "react-router-dom"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { toast } from "sonner"; @@ -48,6 +51,13 @@ import { Input } from "@/components/ui/input"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; import { Drawer, DrawerClose, DrawerContent } from "@/components/ui/drawer"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; import { cn } from "@/lib/utils"; import { copyTextToClipboard, formatJobForWebhook } from "@client/lib/jobCopy"; import { PipelineProgress, DiscoveredPanel } from "../components"; @@ -293,6 +303,8 @@ const ScoreMeter: React.FC<{ score: number | null }> = ({ score }) => { }; export const OrchestratorPage: React.FC = () => { + const location = useLocation(); + const [navOpen, setNavOpen] = useState(false); const [jobs, setJobs] = useState([]); const [stats, setStats] = useState>({ discovered: 0, @@ -302,6 +314,13 @@ export const OrchestratorPage: React.FC = () => { skipped: 0, expired: 0, }); + + const navLinks = [ + { to: "/", label: "Dashboard", icon: Home }, + { to: "/visa-sponsors", label: "Visa Sponsors", icon: Shield }, + { to: "/ukvisajobs", label: "UK Visa Jobs", icon: Briefcase }, + { to: "/settings", label: "Settings", icon: Settings }, + ]; const [isLoading, setIsLoading] = useState(true); const [isPipelineRunning, setIsPipelineRunning] = useState(false); const [processingJobId, setProcessingJobId] = useState(null); @@ -1044,49 +1063,67 @@ export const OrchestratorPage: React.FC = () => { return ( <>
-
-
-
- -
-
-
Job Ops
-
Orchestrator
+
+
+ + + + + + + Navigation + + + + + +
+
+ +
+
+
Job Ops
+
Orchestrator
+
+ {isPipelineRunning && ( - + Pipeline running )}
-
- - - - -
+
+
diff --git a/orchestrator/src/client/pages/SettingsPage.tsx b/orchestrator/src/client/pages/SettingsPage.tsx index a01d1f7..a993ffe 100644 --- a/orchestrator/src/client/pages/SettingsPage.tsx +++ b/orchestrator/src/client/pages/SettingsPage.tsx @@ -3,7 +3,7 @@ */ import React, { useEffect, useMemo, useState } from "react" -import { AlertTriangle, ArrowLeft, Settings, Trash2 } from "lucide-react" +import { AlertTriangle, Settings, Trash2 } from "lucide-react" import { toast } from "sonner" import { PageHeader } from "../components/layout" @@ -418,7 +418,6 @@ export const SettingsPage: React.FC = () => { icon={Settings} title="Settings" subtitle="Configure runtime behavior for this app." - nav={[{ icon: ArrowLeft, label: "Back to Orchestrator", to: "/" }]} />
diff --git a/orchestrator/src/client/pages/UkVisaJobsPage.tsx b/orchestrator/src/client/pages/UkVisaJobsPage.tsx index 1ebdc2f..321056b 100644 --- a/orchestrator/src/client/pages/UkVisaJobsPage.tsx +++ b/orchestrator/src/client/pages/UkVisaJobsPage.tsx @@ -4,7 +4,6 @@ import React, { useEffect, useMemo, useState } from "react"; import { - ArrowLeft, Briefcase, Calendar, ChevronLeft, @@ -14,12 +13,15 @@ import { DollarSign, ExternalLink, GraduationCap, + Home, Loader2, MapPin, + Menu, Search, Settings, + Shield, } from "lucide-react"; -import { Link } from "react-router-dom"; +import { Link, useLocation } from "react-router-dom"; import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; @@ -28,6 +30,13 @@ import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; import { Drawer, DrawerClose, DrawerContent } from "@/components/ui/drawer"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; import { cn } from "@/lib/utils"; import * as api from "../api"; import type { CreateJobInput } from "../../shared/types"; @@ -72,7 +81,16 @@ const clampText = (value: string, max = 160) => (value.length > max ? `${value.s const jobKey = (job: CreateJobInput) => job.sourceJobId || job.jobUrl; +const navLinks = [ + { to: "/", label: "Dashboard", icon: Home }, + { to: "/visa-sponsors", label: "Visa Sponsors", icon: Shield }, + { to: "/ukvisajobs", label: "UK Visa Jobs", icon: Briefcase }, + { to: "/settings", label: "Settings", icon: Settings }, +]; + export const UkVisaJobsPage: React.FC = () => { + const location = useLocation(); + const [navOpen, setNavOpen] = useState(false); const [searchTermInput, setSearchTermInput] = useState(""); const [activeSearchTerm, setActiveSearchTerm] = useState(null); const [results, setResults] = useState([]); @@ -333,8 +351,40 @@ export const UkVisaJobsPage: React.FC = () => { return ( <>
-
-
+
+
+ + + + + + + Navigation + + + + +
@@ -342,23 +392,10 @@ export const UkVisaJobsPage: React.FC = () => {
UK Visa Jobs
Live search console
- + API search
- -
- - -
diff --git a/orchestrator/src/client/pages/VisaSponsorsPage.tsx b/orchestrator/src/client/pages/VisaSponsorsPage.tsx index 40fcdc7..00c1a10 100644 --- a/orchestrator/src/client/pages/VisaSponsorsPage.tsx +++ b/orchestrator/src/client/pages/VisaSponsorsPage.tsx @@ -16,7 +16,6 @@ import { MapPin, Search, Shield, - Sparkles, X, } from "lucide-react"; import { toast } from "sonner"; @@ -320,7 +319,6 @@ export const VisaSponsorsPage: React.FC = () => { title="Visa Sponsors" subtitle="UK Register Search" statusIndicator={isUpdateInProgress ? : undefined} - nav={[{ icon: Sparkles, label: "Back to Orchestrator", to: "/" }]} actions={ <> {status && ( diff --git a/orchestrator/src/components/ui/sheet.tsx b/orchestrator/src/components/ui/sheet.tsx new file mode 100644 index 0000000..02942db --- /dev/null +++ b/orchestrator/src/components/ui/sheet.tsx @@ -0,0 +1,138 @@ +import * as React from "react" +import * as SheetPrimitive from "@radix-ui/react-dialog" +import { cva, type VariantProps } from "class-variance-authority" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Sheet = SheetPrimitive.Root + +const SheetTrigger = SheetPrimitive.Trigger + +const SheetClose = SheetPrimitive.Close + +const SheetPortal = SheetPrimitive.Portal + +const SheetOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetOverlay.displayName = SheetPrimitive.Overlay.displayName + +const sheetVariants = cva( + "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out", + { + variants: { + side: { + top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", + bottom: + "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", + left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", + right: + "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", + }, + }, + defaultVariants: { + side: "right", + }, + } +) + +interface SheetContentProps + extends React.ComponentPropsWithoutRef, + VariantProps {} + +const SheetContent = React.forwardRef< + React.ElementRef, + SheetContentProps +>(({ side = "right", className, children, ...props }, ref) => ( + + + + + + Close + + {children} + + +)) +SheetContent.displayName = SheetPrimitive.Content.displayName + +const SheetHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +SheetHeader.displayName = "SheetHeader" + +const SheetFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +SheetFooter.displayName = "SheetFooter" + +const SheetTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetTitle.displayName = SheetPrimitive.Title.displayName + +const SheetDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetDescription.displayName = SheetPrimitive.Description.displayName + +export { + Sheet, + SheetPortal, + SheetOverlay, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/orchestrator/src/index.css b/orchestrator/src/index.css index 977af18..d46bbd9 100644 --- a/orchestrator/src/index.css +++ b/orchestrator/src/index.css @@ -132,6 +132,68 @@ --shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10); --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25); --tracking-normal: 0em; + + --animate-in: in 0.5s ease-out forwards; + --animate-out: out 0.3s ease-in forwards; + --animate-fade-in: fade-in 0.5s ease-out forwards; + --animate-fade-out: fade-out 0.3s ease-in forwards; + --animate-slide-in-from-left: slide-in-from-left 0.5s ease-out forwards; + --animate-slide-out-to-left: slide-out-to-left 0.3s ease-in forwards; + --animate-slide-in-from-right: slide-in-from-right 0.5s ease-out forwards; + --animate-slide-out-to-right: slide-out-to-right 0.3s ease-in forwards; + --animate-slide-in-from-top: slide-in-from-top 0.5s ease-out forwards; + --animate-slide-out-to-top: slide-out-to-top 0.3s ease-in forwards; + --animate-slide-in-from-bottom: slide-in-from-bottom 0.5s ease-out forwards; + --animate-slide-out-to-bottom: slide-out-to-bottom 0.3s ease-in forwards; + + @keyframes in { + from { opacity: 0; } + to { opacity: 1; } + } + @keyframes out { + from { opacity: 1; } + to { opacity: 0; } + } + @keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } + } + @keyframes fade-out { + from { opacity: 1; } + to { opacity: 0; } + } + @keyframes slide-in-from-left { + from { transform: translateX(-100%); } + to { transform: translateX(0); } + } + @keyframes slide-out-to-left { + from { transform: translateX(0); } + to { transform: translateX(-100%); } + } + @keyframes slide-in-from-right { + from { transform: translateX(100%); } + to { transform: translateX(0); } + } + @keyframes slide-out-to-right { + from { transform: translateX(0); } + to { transform: translateX(100%); } + } + @keyframes slide-in-from-top { + from { transform: translateY(-100%); } + to { transform: translateY(0); } + } + @keyframes slide-out-to-top { + from { transform: translateY(0); } + to { transform: translateY(-100%); } + } + @keyframes slide-in-from-bottom { + from { transform: translateY(100%); } + to { transform: translateY(0); } + } + @keyframes slide-out-to-bottom { + from { transform: translateY(0); } + to { transform: translateY(100%); } + } } .dark {