reduce prop drilling
This commit is contained in:
@@ -7,10 +7,11 @@ import { cn, formatDate, sourceLabel } from "@/lib/utils";
|
|||||||
import type { Job, JobStatus } from "../../shared/types";
|
import type { Job, JobStatus } from "../../shared/types";
|
||||||
import { defaultStatusToken, statusTokens } from "../pages/orchestrator/constants";
|
import { defaultStatusToken, statusTokens } from "../pages/orchestrator/constants";
|
||||||
|
|
||||||
|
import { useSettings } from "../hooks/useSettings";
|
||||||
|
|
||||||
interface JobHeaderProps {
|
interface JobHeaderProps {
|
||||||
job: Job;
|
job: Job;
|
||||||
className?: string;
|
className?: string;
|
||||||
showSponsorInfo?: boolean;
|
|
||||||
onCheckSponsor?: () => Promise<void>;
|
onCheckSponsor?: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,16 +84,16 @@ const SponsorPill: React.FC<SponsorPillProps> = ({ score, names, onCheck }) => {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-5 px-1.5 text-[9px] font-medium text-muted-foreground hover:text-foreground inline-flex items-center gap-1"
|
className="h-5 px-1.5 text-xs font-medium text-muted-foreground hover:text-foreground inline-flex items-center gap-1"
|
||||||
onClick={handleCheck}
|
onClick={handleCheck}
|
||||||
disabled={isChecking}
|
disabled={isChecking}
|
||||||
>
|
>
|
||||||
{isChecking ? (
|
{isChecking ? (
|
||||||
<Loader2 className="h-2.5 w-2.5 animate-spin" />
|
<Loader2 className="h-2 w-2 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<Search className="h-2.5 w-2.5" />
|
<Search className="h-2 w-2" />
|
||||||
)}
|
)}
|
||||||
<span>{isChecking ? "Checking..." : "Check Visa"}</span>
|
<span>{isChecking ? "Checking..." : "Check Sponsorship Status"}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="top">
|
<TooltipContent side="top">
|
||||||
@@ -133,7 +134,8 @@ const SponsorPill: React.FC<SponsorPillProps> = ({ score, names, onCheck }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const JobHeader: React.FC<JobHeaderProps> = ({ job, className, showSponsorInfo = true, onCheckSponsor }) => {
|
export const JobHeader: React.FC<JobHeaderProps> = ({ job, className, onCheckSponsor }) => {
|
||||||
|
const { showSponsorInfo } = useSettings();
|
||||||
const deadline = formatDate(job.deadline);
|
const deadline = formatDate(job.deadline);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ interface ReadyPanelProps {
|
|||||||
onJobMoved: (jobId: string) => void;
|
onJobMoved: (jobId: string) => void;
|
||||||
onEditTailoring: () => void;
|
onEditTailoring: () => void;
|
||||||
onEditDescription: () => void;
|
onEditDescription: () => void;
|
||||||
showSponsorInfo?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const safeFilenamePart = (value: string | null | undefined) =>
|
const safeFilenamePart = (value: string | null | undefined) =>
|
||||||
@@ -62,7 +61,6 @@ export const ReadyPanel: React.FC<ReadyPanelProps> = ({
|
|||||||
onJobMoved,
|
onJobMoved,
|
||||||
onEditTailoring,
|
onEditTailoring,
|
||||||
onEditDescription,
|
onEditDescription,
|
||||||
showSponsorInfo,
|
|
||||||
}) => {
|
}) => {
|
||||||
const [isMarkingApplied, setIsMarkingApplied] = useState(false);
|
const [isMarkingApplied, setIsMarkingApplied] = useState(false);
|
||||||
const [isRegenerating, setIsRegenerating] = useState(false);
|
const [isRegenerating, setIsRegenerating] = useState(false);
|
||||||
@@ -224,7 +222,6 @@ export const ReadyPanel: React.FC<ReadyPanelProps> = ({
|
|||||||
await api.checkSponsor(job.id);
|
await api.checkSponsor(job.id);
|
||||||
await onJobUpdated();
|
await onJobUpdated();
|
||||||
}}
|
}}
|
||||||
showSponsorInfo={showSponsorInfo}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* ─────────────────────────────────────────────────────────────────────
|
{/* ─────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ interface DecideModeProps {
|
|||||||
onSkip: () => void;
|
onSkip: () => void;
|
||||||
isSkipping: boolean;
|
isSkipping: boolean;
|
||||||
onCheckSponsor?: () => Promise<void>;
|
onCheckSponsor?: () => Promise<void>;
|
||||||
showSponsorInfo?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DecideMode: React.FC<DecideModeProps> = ({
|
export const DecideMode: React.FC<DecideModeProps> = ({
|
||||||
@@ -24,7 +23,6 @@ export const DecideMode: React.FC<DecideModeProps> = ({
|
|||||||
onSkip,
|
onSkip,
|
||||||
isSkipping,
|
isSkipping,
|
||||||
onCheckSponsor,
|
onCheckSponsor,
|
||||||
showSponsorInfo,
|
|
||||||
}) => {
|
}) => {
|
||||||
const [showDescription, setShowDescription] = useState(false);
|
const [showDescription, setShowDescription] = useState(false);
|
||||||
const jobLink = job.applicationLink || job.jobUrl;
|
const jobLink = job.applicationLink || job.jobUrl;
|
||||||
@@ -40,7 +38,6 @@ export const DecideMode: React.FC<DecideModeProps> = ({
|
|||||||
<JobHeader
|
<JobHeader
|
||||||
job={job}
|
job={job}
|
||||||
onCheckSponsor={onCheckSponsor}
|
onCheckSponsor={onCheckSponsor}
|
||||||
showSponsorInfo={showSponsorInfo}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='flex flex-col gap-2.5 pt-2 sm:flex-row'>
|
<div className='flex flex-col gap-2.5 pt-2 sm:flex-row'>
|
||||||
|
|||||||
@@ -14,14 +14,12 @@ interface DiscoveredPanelProps {
|
|||||||
job: Job | null;
|
job: Job | null;
|
||||||
onJobUpdated: () => void | Promise<void>;
|
onJobUpdated: () => void | Promise<void>;
|
||||||
onJobMoved: (jobId: string) => void;
|
onJobMoved: (jobId: string) => void;
|
||||||
showSponsorInfo?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DiscoveredPanel: React.FC<DiscoveredPanelProps> = ({
|
export const DiscoveredPanel: React.FC<DiscoveredPanelProps> = ({
|
||||||
job,
|
job,
|
||||||
onJobUpdated,
|
onJobUpdated,
|
||||||
onJobMoved,
|
onJobMoved,
|
||||||
showSponsorInfo,
|
|
||||||
}) => {
|
}) => {
|
||||||
const [mode, setMode] = useState<PanelMode>("decide");
|
const [mode, setMode] = useState<PanelMode>("decide");
|
||||||
const [isSkipping, setIsSkipping] = useState(false);
|
const [isSkipping, setIsSkipping] = useState(false);
|
||||||
@@ -91,7 +89,6 @@ export const DiscoveredPanel: React.FC<DiscoveredPanelProps> = ({
|
|||||||
await api.checkSponsor(job.id);
|
await api.checkSponsor(job.id);
|
||||||
await onJobUpdated();
|
await onJobUpdated();
|
||||||
}}
|
}}
|
||||||
showSponsorInfo={showSponsorInfo}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TailorMode
|
<TailorMode
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import type { AppSettings } from '../../shared/types';
|
||||||
|
import * as api from '../api';
|
||||||
|
|
||||||
|
let settingsCache: AppSettings | null = null;
|
||||||
|
let subscribers: Set<(settings: AppSettings) => void> = new Set();
|
||||||
|
let isFetching = false;
|
||||||
|
|
||||||
|
export function useSettings() {
|
||||||
|
const [settings, setSettings] = useState<AppSettings | null>(settingsCache);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (settingsCache) {
|
||||||
|
setSettings(settingsCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUpdate = (newSettings: AppSettings) => {
|
||||||
|
setSettings(newSettings);
|
||||||
|
};
|
||||||
|
|
||||||
|
subscribers.add(handleUpdate);
|
||||||
|
|
||||||
|
if (!settingsCache && !isFetching) {
|
||||||
|
isFetching = true;
|
||||||
|
api.getSettings()
|
||||||
|
.then((data) => {
|
||||||
|
settingsCache = data;
|
||||||
|
subscribers.forEach(sub => sub(data));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
isFetching = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
subscribers.delete(handleUpdate);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const refreshSettings = async () => {
|
||||||
|
isFetching = true;
|
||||||
|
try {
|
||||||
|
const data = await api.getSettings();
|
||||||
|
settingsCache = data;
|
||||||
|
subscribers.forEach(sub => sub(data));
|
||||||
|
return data;
|
||||||
|
} finally {
|
||||||
|
isFetching = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
settings,
|
||||||
|
isLoading: !settings && isFetching,
|
||||||
|
showSponsorInfo: settings?.showSponsorInfo ?? true,
|
||||||
|
refreshSettings,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -122,9 +122,7 @@ export const OrchestratorPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { pipelineSources, setPipelineSources, toggleSource } = usePipelineSources();
|
const { pipelineSources, setPipelineSources, toggleSource } = usePipelineSources();
|
||||||
const { jobs, stats, settings, isLoading, isPipelineRunning, setIsPipelineRunning, loadJobs } = useOrchestratorData();
|
const { jobs, stats, isLoading, isPipelineRunning, setIsPipelineRunning, loadJobs } = useOrchestratorData();
|
||||||
|
|
||||||
const showSponsorInfo = settings?.showSponsorInfo ?? true;
|
|
||||||
|
|
||||||
const activeJobs = useFilteredJobs(jobs, activeTab, sourceFilter, searchQuery, sort);
|
const activeJobs = useFilteredJobs(jobs, activeTab, sourceFilter, searchQuery, sort);
|
||||||
const counts = useMemo(() => getJobCounts(jobs), [jobs]);
|
const counts = useMemo(() => getJobCounts(jobs), [jobs]);
|
||||||
@@ -278,7 +276,6 @@ export const OrchestratorPage: React.FC = () => {
|
|||||||
onSelectJobId={handleSelectJobId}
|
onSelectJobId={handleSelectJobId}
|
||||||
onJobUpdated={loadJobs}
|
onJobUpdated={loadJobs}
|
||||||
onSetActiveTab={setActiveTab}
|
onSetActiveTab={setActiveTab}
|
||||||
showSponsorInfo={showSponsorInfo}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ interface JobDetailPanelProps {
|
|||||||
onSelectJobId: (jobId: string | null) => void;
|
onSelectJobId: (jobId: string | null) => void;
|
||||||
onJobUpdated: () => Promise<void>;
|
onJobUpdated: () => Promise<void>;
|
||||||
onSetActiveTab: (tab: FilterTab) => void;
|
onSetActiveTab: (tab: FilterTab) => void;
|
||||||
showSponsorInfo?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const JobDetailPanel: React.FC<JobDetailPanelProps> = ({
|
export const JobDetailPanel: React.FC<JobDetailPanelProps> = ({
|
||||||
@@ -51,7 +50,6 @@ export const JobDetailPanel: React.FC<JobDetailPanelProps> = ({
|
|||||||
onSelectJobId,
|
onSelectJobId,
|
||||||
onJobUpdated,
|
onJobUpdated,
|
||||||
onSetActiveTab,
|
onSetActiveTab,
|
||||||
showSponsorInfo,
|
|
||||||
}) => {
|
}) => {
|
||||||
const [detailTab, setDetailTab] = useState<"overview" | "tailoring" | "description">("overview");
|
const [detailTab, setDetailTab] = useState<"overview" | "tailoring" | "description">("overview");
|
||||||
const [isEditingDescription, setIsEditingDescription] = useState(false);
|
const [isEditingDescription, setIsEditingDescription] = useState(false);
|
||||||
@@ -235,7 +233,6 @@ export const JobDetailPanel: React.FC<JobDetailPanelProps> = ({
|
|||||||
job={selectedJob}
|
job={selectedJob}
|
||||||
onJobUpdated={onJobUpdated}
|
onJobUpdated={onJobUpdated}
|
||||||
onJobMoved={handleJobMoved}
|
onJobMoved={handleJobMoved}
|
||||||
showSponsorInfo={showSponsorInfo}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -257,7 +254,6 @@ export const JobDetailPanel: React.FC<JobDetailPanelProps> = ({
|
|||||||
setIsEditingDescription(true);
|
setIsEditingDescription(true);
|
||||||
}, 50);
|
}, 50);
|
||||||
}}
|
}}
|
||||||
showSponsorInfo={showSponsorInfo}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -279,7 +275,6 @@ export const JobDetailPanel: React.FC<JobDetailPanelProps> = ({
|
|||||||
await api.checkSponsor(selectedJob.id);
|
await api.checkSponsor(selectedJob.id);
|
||||||
await onJobUpdated();
|
await onJobUpdated();
|
||||||
}}
|
}}
|
||||||
showSponsorInfo={showSponsorInfo}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-1.5">
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import type { Job, JobStatus, AppSettings } from "../../../shared/types";
|
import type { Job, JobStatus } from "../../../shared/types";
|
||||||
import * as api from "../../api";
|
import * as api from "../../api";
|
||||||
|
|
||||||
const initialStats: Record<JobStatus, number> = {
|
const initialStats: Record<JobStatus, number> = {
|
||||||
@@ -16,20 +16,15 @@ const initialStats: Record<JobStatus, number> = {
|
|||||||
export const useOrchestratorData = () => {
|
export const useOrchestratorData = () => {
|
||||||
const [jobs, setJobs] = useState<Job[]>([]);
|
const [jobs, setJobs] = useState<Job[]>([]);
|
||||||
const [stats, setStats] = useState<Record<JobStatus, number>>(initialStats);
|
const [stats, setStats] = useState<Record<JobStatus, number>>(initialStats);
|
||||||
const [settings, setSettings] = useState<AppSettings | null>(null);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isPipelineRunning, setIsPipelineRunning] = useState(false);
|
const [isPipelineRunning, setIsPipelineRunning] = useState(false);
|
||||||
|
|
||||||
const loadJobs = useCallback(async () => {
|
const loadJobs = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const [jobsData, settingsData] = await Promise.all([
|
const data = await api.getJobs();
|
||||||
api.getJobs(),
|
setJobs(data.jobs);
|
||||||
api.getSettings(),
|
setStats(data.byStatus);
|
||||||
]);
|
|
||||||
setJobs(jobsData.jobs);
|
|
||||||
setStats(jobsData.byStatus);
|
|
||||||
setSettings(settingsData);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Failed to load jobs";
|
const message = error instanceof Error ? error.message : "Failed to load jobs";
|
||||||
toast.error(message);
|
toast.error(message);
|
||||||
@@ -59,5 +54,5 @@ export const useOrchestratorData = () => {
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [loadJobs, checkPipelineStatus]);
|
}, [loadJobs, checkPipelineStatus]);
|
||||||
|
|
||||||
return { jobs, stats, settings, isLoading, isPipelineRunning, setIsPipelineRunning, loadJobs, checkPipelineStatus };
|
return { jobs, stats, isLoading, isPipelineRunning, setIsPipelineRunning, loadJobs, checkPipelineStatus };
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user