sheet implemented
This commit is contained in:
parent
ff4cdb4101
commit
5bb4c67d10
@ -7,7 +7,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
|
"dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
|
||||||
"dev:server": "tsx watch src/server/index.ts",
|
"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": "npm run build:client && npm run build:server",
|
||||||
"build:server": "tsc -p tsconfig.server.json",
|
"build:server": "tsc -p tsconfig.server.json",
|
||||||
"build:client": "vite build",
|
"build:client": "vite build",
|
||||||
|
|||||||
@ -4,28 +4,19 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
|
Briefcase,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
|
Home,
|
||||||
Loader2,
|
Loader2,
|
||||||
|
Menu,
|
||||||
Play,
|
Play,
|
||||||
RefreshCcw,
|
RefreshCcw,
|
||||||
Settings,
|
Settings,
|
||||||
Shield,
|
Shield,
|
||||||
Trash2,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
@ -35,6 +26,13 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from "@/components/ui/sheet";
|
||||||
import type { JobSource } from "../../shared/types";
|
import type { JobSource } from "../../shared/types";
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
@ -54,6 +52,9 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
pipelineSources,
|
pipelineSources,
|
||||||
onPipelineSourcesChange,
|
onPipelineSourcesChange,
|
||||||
}) => {
|
}) => {
|
||||||
|
const location = useLocation();
|
||||||
|
const [sheetOpen, setSheetOpen] = React.useState(false);
|
||||||
|
|
||||||
const sourceLabel: Record<JobSource, string> = {
|
const sourceLabel: Record<JobSource, string> = {
|
||||||
gradcracker: "Gradcracker",
|
gradcracker: "Gradcracker",
|
||||||
indeed: "Indeed",
|
indeed: "Indeed",
|
||||||
@ -63,6 +64,13 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
|
|
||||||
const orderedSources: JobSource[] = ["gradcracker", "indeed", "linkedin", "ukvisajobs"];
|
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 toggleSource = (source: JobSource, checked: boolean) => {
|
||||||
const next = checked
|
const next = checked
|
||||||
? Array.from(new Set([...pipelineSources, source]))
|
? Array.from(new Set([...pipelineSources, source]))
|
||||||
@ -75,6 +83,38 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
return (
|
return (
|
||||||
<header className='sticky top-0 z-40 border-b bg-background/80 backdrop-blur supports-[backdrop-filter]:bg-background/60'>
|
<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='container mx-auto flex max-w-7xl items-center justify-between gap-4 px-4 py-4'>
|
||||||
|
<div className='flex items-center gap-3'>
|
||||||
|
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
||||||
|
<SheetTrigger asChild>
|
||||||
|
<Button variant='ghost' size='icon'>
|
||||||
|
<Menu className='h-5 w-5' />
|
||||||
|
<span className='sr-only'>Open navigation menu</span>
|
||||||
|
</Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent side='left' className='w-64'>
|
||||||
|
<SheetHeader>
|
||||||
|
<SheetTitle>Navigation</SheetTitle>
|
||||||
|
</SheetHeader>
|
||||||
|
<nav className='mt-6 flex flex-col gap-2'>
|
||||||
|
{navLinks.map(({ to, label, icon: Icon }) => (
|
||||||
|
<Link
|
||||||
|
key={to}
|
||||||
|
to={to}
|
||||||
|
onClick={() => setSheetOpen(false)}
|
||||||
|
className={`flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground ${
|
||||||
|
location.pathname === to
|
||||||
|
? "bg-accent text-accent-foreground"
|
||||||
|
: "text-muted-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon className='h-4 w-4' />
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
to='/'
|
to='/'
|
||||||
className='flex items-center gap-3 hover:opacity-80 transition-opacity'
|
className='flex items-center gap-3 hover:opacity-80 transition-opacity'
|
||||||
@ -91,6 +131,7 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
<div className='text-xs text-muted-foreground'>Orchestrator</div>
|
<div className='text-xs text-muted-foreground'>Orchestrator</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-wrap items-center gap-1.5'>
|
<div className='flex flex-wrap items-center gap-1.5'>
|
||||||
<Button
|
<Button
|
||||||
@ -103,28 +144,6 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
<span className='hidden sm:inline'>Refresh</span>
|
<span className='hidden sm:inline'>Refresh</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
|
||||||
asChild
|
|
||||||
variant='outline'
|
|
||||||
size='sm'
|
|
||||||
>
|
|
||||||
<Link to='/visa-sponsors'>
|
|
||||||
<Shield className='h-4 w-4' />
|
|
||||||
<span className='hidden sm:inline'>Visa Sponsors</span>
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
asChild
|
|
||||||
variant='outline'
|
|
||||||
size='sm'
|
|
||||||
>
|
|
||||||
<Link to='/settings'>
|
|
||||||
<Settings className='h-4 w-4' />
|
|
||||||
<span className='hidden sm:inline'>Settings</span>
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
size='sm'
|
size='sm'
|
||||||
|
|||||||
@ -2,23 +2,31 @@
|
|||||||
* Shared layout components for consistent page structure.
|
* Shared layout components for consistent page structure.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link, useLocation } from "react-router-dom";
|
||||||
import { LucideIcon } from "lucide-react";
|
import { Briefcase, Home, LucideIcon, Menu, Settings, Shield } from "lucide-react";
|
||||||
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from "@/components/ui/sheet";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Page Header
|
// Page Header
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
interface HeaderNavItem {
|
const navLinks = [
|
||||||
icon: LucideIcon;
|
{ to: "/", label: "Dashboard", icon: Home },
|
||||||
label: string;
|
{ to: "/visa-sponsors", label: "Visa Sponsors", icon: Shield },
|
||||||
to: string;
|
{ to: "/ukvisajobs", label: "UK Visa Jobs", icon: Briefcase },
|
||||||
}
|
{ to: "/settings", label: "Settings", icon: Settings },
|
||||||
|
];
|
||||||
|
|
||||||
interface PageHeaderProps {
|
interface PageHeaderProps {
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
@ -26,7 +34,6 @@ interface PageHeaderProps {
|
|||||||
subtitle: string;
|
subtitle: string;
|
||||||
badge?: string;
|
badge?: string;
|
||||||
statusIndicator?: React.ReactNode;
|
statusIndicator?: React.ReactNode;
|
||||||
nav?: HeaderNavItem[];
|
|
||||||
actions?: React.ReactNode;
|
actions?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,12 +43,47 @@ export const PageHeader: React.FC<PageHeaderProps> = ({
|
|||||||
subtitle,
|
subtitle,
|
||||||
badge,
|
badge,
|
||||||
statusIndicator,
|
statusIndicator,
|
||||||
nav,
|
|
||||||
actions,
|
actions,
|
||||||
}) => (
|
}) => {
|
||||||
|
const location = useLocation();
|
||||||
|
const [navOpen, setNavOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
<header className="sticky top-0 z-40 border-b bg-background/80 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
<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 flex-col gap-3 px-4 py-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="container mx-auto flex max-w-7xl items-center justify-between gap-4 px-4 py-4">
|
||||||
<div className="flex min-w-0 flex-wrap items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
<Sheet open={navOpen} onOpenChange={setNavOpen}>
|
||||||
|
<SheetTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<Menu className="h-5 w-5" />
|
||||||
|
<span className="sr-only">Open navigation menu</span>
|
||||||
|
</Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent side="left" className="w-64">
|
||||||
|
<SheetHeader>
|
||||||
|
<SheetTitle>Navigation</SheetTitle>
|
||||||
|
</SheetHeader>
|
||||||
|
<nav className="mt-6 flex flex-col gap-2">
|
||||||
|
{navLinks.map(({ to, label, icon: NavIcon }) => (
|
||||||
|
<Link
|
||||||
|
key={to}
|
||||||
|
to={to}
|
||||||
|
onClick={() => setNavOpen(false)}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground",
|
||||||
|
location.pathname === to
|
||||||
|
? "bg-accent text-accent-foreground"
|
||||||
|
: "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<NavIcon className="h-4 w-4" />
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
|
||||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg border border-border/60 bg-muted/30">
|
<div className="flex h-9 w-9 items-center justify-center rounded-lg border border-border/60 bg-muted/30">
|
||||||
<Icon className="h-4 w-4 text-muted-foreground" />
|
<Icon className="h-4 w-4 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
@ -57,19 +99,13 @@ export const PageHeader: React.FC<PageHeaderProps> = ({
|
|||||||
{statusIndicator}
|
{statusIndicator}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full flex-wrap items-center gap-2 sm:w-auto sm:justify-end">
|
<div className="flex items-center gap-2">
|
||||||
{nav?.map((item) => (
|
|
||||||
<Button key={item.to} asChild variant="ghost" size="icon" aria-label={item.label}>
|
|
||||||
<Link to={item.to}>
|
|
||||||
<item.icon className="h-4 w-4" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
{actions}
|
{actions}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Status Indicator (Pipeline running, Updating, etc.)
|
// Status Indicator (Pipeline running, Updating, etc.)
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
ArrowUpDown,
|
ArrowUpDown,
|
||||||
|
Briefcase,
|
||||||
Calendar,
|
Calendar,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
@ -14,8 +15,10 @@ import {
|
|||||||
ExternalLink,
|
ExternalLink,
|
||||||
FileText,
|
FileText,
|
||||||
Filter,
|
Filter,
|
||||||
|
Home,
|
||||||
Loader2,
|
Loader2,
|
||||||
MapPin,
|
MapPin,
|
||||||
|
Menu,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
Play,
|
Play,
|
||||||
RefreshCcw,
|
RefreshCcw,
|
||||||
@ -26,7 +29,7 @@ import {
|
|||||||
Sparkles,
|
Sparkles,
|
||||||
XCircle,
|
XCircle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link, useLocation } from "react-router-dom";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@ -48,6 +51,13 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Drawer, DrawerClose, DrawerContent } from "@/components/ui/drawer";
|
import { Drawer, DrawerClose, DrawerContent } from "@/components/ui/drawer";
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from "@/components/ui/sheet";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { copyTextToClipboard, formatJobForWebhook } from "@client/lib/jobCopy";
|
import { copyTextToClipboard, formatJobForWebhook } from "@client/lib/jobCopy";
|
||||||
import { PipelineProgress, DiscoveredPanel } from "../components";
|
import { PipelineProgress, DiscoveredPanel } from "../components";
|
||||||
@ -293,6 +303,8 @@ const ScoreMeter: React.FC<{ score: number | null }> = ({ score }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const OrchestratorPage: React.FC = () => {
|
export const OrchestratorPage: React.FC = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
const [navOpen, setNavOpen] = useState(false);
|
||||||
const [jobs, setJobs] = useState<Job[]>([]);
|
const [jobs, setJobs] = useState<Job[]>([]);
|
||||||
const [stats, setStats] = useState<Record<JobStatus, number>>({
|
const [stats, setStats] = useState<Record<JobStatus, number>>({
|
||||||
discovered: 0,
|
discovered: 0,
|
||||||
@ -302,6 +314,13 @@ export const OrchestratorPage: React.FC = () => {
|
|||||||
skipped: 0,
|
skipped: 0,
|
||||||
expired: 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 [isLoading, setIsLoading] = useState(true);
|
||||||
const [isPipelineRunning, setIsPipelineRunning] = useState(false);
|
const [isPipelineRunning, setIsPipelineRunning] = useState(false);
|
||||||
const [processingJobId, setProcessingJobId] = useState<string | null>(null);
|
const [processingJobId, setProcessingJobId] = useState<string | null>(null);
|
||||||
@ -1044,8 +1063,40 @@ export const OrchestratorPage: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className="sticky top-0 z-40 border-b bg-background/80 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
<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 flex-col gap-3 px-4 py-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="container mx-auto flex max-w-7xl items-center justify-between gap-4 px-4 py-4">
|
||||||
<div className="flex min-w-0 flex-wrap items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
<Sheet open={navOpen} onOpenChange={setNavOpen}>
|
||||||
|
<SheetTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<Menu className="h-5 w-5" />
|
||||||
|
<span className="sr-only">Open navigation menu</span>
|
||||||
|
</Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent side="left" className="w-64">
|
||||||
|
<SheetHeader>
|
||||||
|
<SheetTitle>Navigation</SheetTitle>
|
||||||
|
</SheetHeader>
|
||||||
|
<nav className="mt-6 flex flex-col gap-2">
|
||||||
|
{navLinks.map(({ to, label, icon: Icon }) => (
|
||||||
|
<Link
|
||||||
|
key={to}
|
||||||
|
to={to}
|
||||||
|
onClick={() => setNavOpen(false)}
|
||||||
|
className={`flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground ${
|
||||||
|
location.pathname === to
|
||||||
|
? "bg-accent text-accent-foreground"
|
||||||
|
: "text-muted-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4" />
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
|
||||||
|
<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">
|
<div className="flex h-9 w-9 items-center justify-center rounded-lg border border-border/60 bg-muted/30">
|
||||||
<Sparkles className="h-4 w-4 text-muted-foreground" />
|
<Sparkles className="h-4 w-4 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
@ -1053,40 +1104,26 @@ export const OrchestratorPage: React.FC = () => {
|
|||||||
<div className="text-sm font-semibold tracking-tight">Job Ops</div>
|
<div className="text-sm font-semibold tracking-tight">Job Ops</div>
|
||||||
<div className="text-xs text-muted-foreground">Orchestrator</div>
|
<div className="text-xs text-muted-foreground">Orchestrator</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{isPipelineRunning && (
|
{isPipelineRunning && (
|
||||||
<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="hidden sm: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" />
|
<span className="h-1.5 w-1.5 rounded-full bg-amber-400 animate-pulse" />
|
||||||
Pipeline running
|
Pipeline running
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full flex-wrap items-center gap-2 sm:w-auto sm:justify-end">
|
<div className="flex items-center gap-2">
|
||||||
<Button asChild variant="ghost" size="icon" aria-label="Visa Sponsors search">
|
<div className="flex items-center gap-1">
|
||||||
<Link to="/visa-sponsors">
|
|
||||||
<Shield className="h-4 w-4" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
<Button asChild variant="ghost" size="icon" aria-label="UK Visa Jobs search">
|
|
||||||
<Link to="/ukvisajobs">
|
|
||||||
<Search className="h-4 w-4" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
<Button asChild variant="ghost" size="icon" aria-label="Settings">
|
|
||||||
<Link to="/settings">
|
|
||||||
<Settings className="h-4 w-4" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="flex w-full items-center gap-1 sm:w-auto">
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleRunPipeline}
|
onClick={handleRunPipeline}
|
||||||
disabled={isPipelineRunning}
|
disabled={isPipelineRunning}
|
||||||
className="w-full gap-2 sm:w-auto"
|
className="gap-2"
|
||||||
>
|
>
|
||||||
{isPipelineRunning ? <Loader2 className="h-4 w-4 animate-spin" /> : <Play className="h-4 w-4" />}
|
{isPipelineRunning ? <Loader2 className="h-4 w-4 animate-spin" /> : <Play className="h-4 w-4" />}
|
||||||
{isPipelineRunning ? "Running" : "Run pipeline"}
|
<span className="hidden sm:inline">{isPipelineRunning ? "Running" : "Run pipeline"}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useMemo, useState } from "react"
|
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 { toast } from "sonner"
|
||||||
|
|
||||||
import { PageHeader } from "../components/layout"
|
import { PageHeader } from "../components/layout"
|
||||||
@ -418,7 +418,6 @@ export const SettingsPage: React.FC = () => {
|
|||||||
icon={Settings}
|
icon={Settings}
|
||||||
title="Settings"
|
title="Settings"
|
||||||
subtitle="Configure runtime behavior for this app."
|
subtitle="Configure runtime behavior for this app."
|
||||||
nav={[{ icon: ArrowLeft, label: "Back to Orchestrator", to: "/" }]}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<main className="container mx-auto max-w-3xl space-y-6 px-4 py-6 pb-12">
|
<main className="container mx-auto max-w-3xl space-y-6 px-4 py-6 pb-12">
|
||||||
|
|||||||
@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
|
||||||
Briefcase,
|
Briefcase,
|
||||||
Calendar,
|
Calendar,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
@ -14,12 +13,15 @@ import {
|
|||||||
DollarSign,
|
DollarSign,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
GraduationCap,
|
GraduationCap,
|
||||||
|
Home,
|
||||||
Loader2,
|
Loader2,
|
||||||
MapPin,
|
MapPin,
|
||||||
|
Menu,
|
||||||
Search,
|
Search,
|
||||||
Settings,
|
Settings,
|
||||||
|
Shield,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link, useLocation } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@ -28,6 +30,13 @@ import { Checkbox } from "@/components/ui/checkbox";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Drawer, DrawerClose, DrawerContent } from "@/components/ui/drawer";
|
import { Drawer, DrawerClose, DrawerContent } from "@/components/ui/drawer";
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from "@/components/ui/sheet";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import * as api from "../api";
|
import * as api from "../api";
|
||||||
import type { CreateJobInput } from "../../shared/types";
|
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 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 = () => {
|
export const UkVisaJobsPage: React.FC = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
const [navOpen, setNavOpen] = useState(false);
|
||||||
const [searchTermInput, setSearchTermInput] = useState("");
|
const [searchTermInput, setSearchTermInput] = useState("");
|
||||||
const [activeSearchTerm, setActiveSearchTerm] = useState<string | null>(null);
|
const [activeSearchTerm, setActiveSearchTerm] = useState<string | null>(null);
|
||||||
const [results, setResults] = useState<CreateJobInput[]>([]);
|
const [results, setResults] = useState<CreateJobInput[]>([]);
|
||||||
@ -333,8 +351,40 @@ export const UkVisaJobsPage: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className="sticky top-0 z-40 border-b bg-background/80 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
<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 flex-col gap-3 px-4 py-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="container mx-auto flex max-w-7xl items-center justify-between gap-4 px-4 py-4">
|
||||||
<div className="flex min-w-0 flex-wrap items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
<Sheet open={navOpen} onOpenChange={setNavOpen}>
|
||||||
|
<SheetTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<Menu className="h-5 w-5" />
|
||||||
|
<span className="sr-only">Open navigation menu</span>
|
||||||
|
</Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent side="left" className="w-64">
|
||||||
|
<SheetHeader>
|
||||||
|
<SheetTitle>Navigation</SheetTitle>
|
||||||
|
</SheetHeader>
|
||||||
|
<nav className="mt-6 flex flex-col gap-2">
|
||||||
|
{navLinks.map(({ to, label, icon: Icon }) => (
|
||||||
|
<Link
|
||||||
|
key={to}
|
||||||
|
to={to}
|
||||||
|
onClick={() => setNavOpen(false)}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground",
|
||||||
|
location.pathname === to
|
||||||
|
? "bg-accent text-accent-foreground"
|
||||||
|
: "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4" />
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
|
||||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg border border-border/60 bg-muted/30">
|
<div className="flex h-9 w-9 items-center justify-center rounded-lg border border-border/60 bg-muted/30">
|
||||||
<Briefcase className="h-4 w-4 text-muted-foreground" />
|
<Briefcase className="h-4 w-4 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
@ -342,23 +392,10 @@ export const UkVisaJobsPage: React.FC = () => {
|
|||||||
<div className="text-sm font-semibold tracking-tight">UK Visa Jobs</div>
|
<div className="text-sm font-semibold tracking-tight">UK Visa Jobs</div>
|
||||||
<div className="text-xs text-muted-foreground">Live search console</div>
|
<div className="text-xs text-muted-foreground">Live search console</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="outline" className="uppercase tracking-wide">
|
<Badge variant="outline" className="hidden sm:inline-flex uppercase tracking-wide">
|
||||||
API search
|
API search
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full items-center gap-2 sm:w-auto sm:justify-end">
|
|
||||||
<Button asChild variant="ghost" size="icon" aria-label="Back to orchestrator">
|
|
||||||
<Link to="/">
|
|
||||||
<ArrowLeft className="h-4 w-4" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
<Button asChild variant="ghost" size="icon" aria-label="Settings">
|
|
||||||
<Link to="/settings">
|
|
||||||
<Settings className="h-4 w-4" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,6 @@ import {
|
|||||||
MapPin,
|
MapPin,
|
||||||
Search,
|
Search,
|
||||||
Shield,
|
Shield,
|
||||||
Sparkles,
|
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@ -320,7 +319,6 @@ export const VisaSponsorsPage: React.FC = () => {
|
|||||||
title="Visa Sponsors"
|
title="Visa Sponsors"
|
||||||
subtitle="UK Register Search"
|
subtitle="UK Register Search"
|
||||||
statusIndicator={isUpdateInProgress ? <StatusIndicator label="Updating" /> : undefined}
|
statusIndicator={isUpdateInProgress ? <StatusIndicator label="Updating" /> : undefined}
|
||||||
nav={[{ icon: Sparkles, label: "Back to Orchestrator", to: "/" }]}
|
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
{status && (
|
{status && (
|
||||||
|
|||||||
138
orchestrator/src/components/ui/sheet.tsx
Normal file
138
orchestrator/src/components/ui/sheet.tsx
Normal file
@ -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<typeof SheetPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SheetPrimitive.Overlay
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={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<typeof SheetPrimitive.Content>,
|
||||||
|
VariantProps<typeof sheetVariants> {}
|
||||||
|
|
||||||
|
const SheetContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||||
|
SheetContentProps
|
||||||
|
>(({ side = "right", className, children, ...props }, ref) => (
|
||||||
|
<SheetPortal>
|
||||||
|
<SheetOverlay />
|
||||||
|
<SheetPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(sheetVariants({ side }), className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</SheetPrimitive.Close>
|
||||||
|
{children}
|
||||||
|
</SheetPrimitive.Content>
|
||||||
|
</SheetPortal>
|
||||||
|
))
|
||||||
|
SheetContent.displayName = SheetPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const SheetHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-2 text-center sm:text-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
SheetHeader.displayName = "SheetHeader"
|
||||||
|
|
||||||
|
const SheetFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
SheetFooter.displayName = "SheetFooter"
|
||||||
|
|
||||||
|
const SheetTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SheetPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-lg font-semibold text-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const SheetDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SheetPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Sheet,
|
||||||
|
SheetPortal,
|
||||||
|
SheetOverlay,
|
||||||
|
SheetTrigger,
|
||||||
|
SheetClose,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetFooter,
|
||||||
|
SheetTitle,
|
||||||
|
SheetDescription,
|
||||||
|
}
|
||||||
@ -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-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);
|
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
|
||||||
--tracking-normal: 0em;
|
--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 {
|
.dark {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user