import { SettingsInput } from "@client/pages/settings/components/SettingsInput"; import type { JobspyValues } from "@client/pages/settings/types"; import type { UpdateSettingsInput } from "@shared/settings-schema"; import type React from "react"; import { Controller, useFormContext } from "react-hook-form"; import { AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; type JobspySectionProps = { values: JobspyValues; isLoading: boolean; isSaving: boolean; }; const JOBSPY_INDEED_COUNTRIES = [ "argentina", "australia", "austria", "bahrain", "bangladesh", "belgium", "bulgaria", "brazil", "canada", "chile", "china", "colombia", "costa rica", "croatia", "cyprus", "czech republic", "czechia", "denmark", "ecuador", "egypt", "estonia", "finland", "france", "germany", "greece", "hong kong", "hungary", "india", "indonesia", "ireland", "israel", "italy", "japan", "kuwait", "latvia", "lithuania", "luxembourg", "malaysia", "malta", "mexico", "morocco", "netherlands", "new zealand", "nigeria", "norway", "oman", "pakistan", "panama", "peru", "philippines", "poland", "portugal", "qatar", "romania", "saudi arabia", "singapore", "slovakia", "slovenia", "south africa", "south korea", "spain", "sweden", "switzerland", "taiwan", "thailand", "türkiye", "turkey", "ukraine", "united arab emirates", "uk", "united kingdom", "usa", "us", "united states", "uruguay", "venezuela", "vietnam", "usa/ca", "worldwide", ]; const COUNTRY_ALIASES: Record = { uk: "united kingdom", us: "united states", usa: "united states", türkiye: "turkey", "czech republic": "czechia", }; const COUNTRY_LABELS: Record = { "united kingdom": "United Kingdom", "united states": "United States", "usa/ca": "USA/CA", turkey: "Turkey", czechia: "Czechia", }; const normalizeCountryValue = (value: string) => COUNTRY_ALIASES[value] ?? value; const formatCountryLabel = (value: string) => COUNTRY_LABELS[value] || value.replace(/\b\w/g, (char) => char.toUpperCase()); const JOBSPY_INDEED_COUNTRY_OPTIONS = Array.from( new Map( JOBSPY_INDEED_COUNTRIES.map((country) => { const normalized = normalizeCountryValue(country); return [normalized, normalized]; }), ).values(), ); export const JobspySection: React.FC = ({ values, isLoading, isSaving, }) => { const { sites, location, resultsWanted, hoursOld, countryIndeed, linkedinFetchDescription, } = values; const { control, register, formState: { errors }, } = useFormContext(); return ( JobSpy Scraper
Scraped Sites
( { const current = field.value ?? sites.default; let next = [...current]; if (checked) { if (!next.includes("indeed")) next.push("indeed"); } else { next = next.filter((s) => s !== "indeed"); } field.onChange(next); }} disabled={isLoading || isSaving} /> )} />
( { const current = field.value ?? sites.default; let next = [...current]; if (checked) { if (!next.includes("linkedin")) next.push("linkedin"); } else { next = next.filter((s) => s !== "linkedin"); } field.onChange(next); }} disabled={isLoading || isSaving} /> )} />
{errors.jobspySites && (

{errors.jobspySites.message}

)}
Select which sites JobSpy should scrape.
Effective: {(sites.effective || []).join(", ") || "None"} Default: {(sites.default || []).join(", ")}
( { const value = parseInt(event.target.value, 10); if (Number.isNaN(value)) { field.onChange(null); } else { field.onChange(Math.min(1000, Math.max(1, value))); } }, }} disabled={isLoading || isSaving} error={ errors.jobspyResultsWanted?.message as string | undefined } helper={`Number of results to fetch per term per site. Default: ${resultsWanted.default}. Max 1000.`} current={`Effective: ${resultsWanted.effective} | Default: ${resultsWanted.default}`} /> )} /> ( { const value = parseInt(event.target.value, 10); if (Number.isNaN(value)) { field.onChange(null); } else { field.onChange(Math.min(720, Math.max(1, value))); } }, }} disabled={isLoading || isSaving} error={errors.jobspyHoursOld?.message as string | undefined} helper={`Max age of jobs in hours (e.g. 72 for 3 days). Default: ${hoursOld.default}. Max 720.`} current={`Effective: ${hoursOld.effective}h | Default: ${hoursOld.default}h`} /> )} /> { const currentValue = ( field.value ?? countryIndeed.default ?? "" ).toLowerCase(); const normalizedValue = normalizeCountryValue(currentValue); const displayValue = JOBSPY_INDEED_COUNTRY_OPTIONS.includes( normalizedValue, ) ? normalizedValue : "__default__"; return (
{errors.jobspyCountryIndeed && (

{errors.jobspyCountryIndeed.message}

)}
Select one of JobSpy's supported Indeed country values.
{`Effective: ${countryIndeed.effective || "—"} | Default: ${countryIndeed.default || "—"}`}
); }} />
( field.onChange(!!checked)} disabled={isLoading || isSaving} /> )} />

If enabled, JobSpy will make extra requests to fetch full descriptions. Slower but better data.

Effective: {linkedinFetchDescription.effective ? "Yes" : "No"} Default: {linkedinFetchDescription.default ? "Yes" : "No"}
); };