reduce prop drilling

This commit is contained in:
DaKheera47
2026-01-20 22:46:50 +00:00
parent 29b259352f
commit 158e4b108b
8 changed files with 72 additions and 34 deletions
@@ -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 };
}; };