/** * MapTab — world map color threshold configuration editor. * * Allows the user to set the low / medium / high ban-count thresholds * that drive country fill colors on the World Map page. */ import { useCallback, useEffect, useMemo, useState } from "react"; import { Field, Input, MessageBar, MessageBarBody, Skeleton, SkeletonItem, Text, tokens, } from "@fluentui/react-components"; import { ApiError } from "../../api/client"; import { fetchMapColorThresholds, updateMapColorThresholds, } from "../../api/config"; import type { MapColorThresholdsResponse, MapColorThresholdsUpdate } from "../../types/config"; import { useAutoSave } from "../../hooks/useAutoSave"; import { AutoSaveIndicator } from "./AutoSaveIndicator"; import { useConfigStyles } from "./configStyles"; // --------------------------------------------------------------------------- // Inner form — only mounted after data is loaded. // --------------------------------------------------------------------------- interface MapFormProps { initial: MapColorThresholdsResponse; } function MapForm({ initial }: MapFormProps): React.JSX.Element { const styles = useConfigStyles(); const [thresholdHigh, setThresholdHigh] = useState(String(initial.threshold_high)); const [thresholdMedium, setThresholdMedium] = useState(String(initial.threshold_medium)); const [thresholdLow, setThresholdLow] = useState(String(initial.threshold_low)); const high = Number(thresholdHigh); const medium = Number(thresholdMedium); const low = Number(thresholdLow); const validationError = useMemo(() => { if (isNaN(high) || isNaN(medium) || isNaN(low)) return "All thresholds must be valid numbers."; if (high <= 0 || medium <= 0 || low <= 0) return "All thresholds must be positive integers."; if (!(high > medium && medium > low)) return "Thresholds must satisfy: high > medium > low."; return null; }, [high, medium, low]); // Only pass a new payload to useAutoSave when all values are valid. const [validPayload, setValidPayload] = useState({ threshold_high: initial.threshold_high, threshold_medium: initial.threshold_medium, threshold_low: initial.threshold_low, }); useEffect(() => { if (validationError !== null) return; setValidPayload({ threshold_high: high, threshold_medium: medium, threshold_low: low }); }, [high, medium, low, validationError]); const saveThresholds = useCallback( async (payload: MapColorThresholdsUpdate): Promise => { await updateMapColorThresholds(payload); }, [], ); const { status: saveStatus, errorText: saveErrorText, retry: retrySave } = useAutoSave(validPayload, saveThresholds); return (
Map Color Thresholds Configure the ban count thresholds that determine country fill colors on the World Map. Countries with zero bans remain transparent. Colors smoothly interpolate between thresholds.
{validationError && ( {validationError} )}
{ setThresholdLow(d.value); }} min={1} /> { setThresholdMedium(d.value); }} min={1} /> { setThresholdHigh(d.value); }} min={1} />
• 1 to {thresholdLow}: Light green → Full green
• {thresholdLow} to {thresholdMedium}: Green → Yellow
• {thresholdMedium} to {thresholdHigh}: Yellow → Red
• {thresholdHigh}+: Solid red
); } // --------------------------------------------------------------------------- // Outer loader component. // --------------------------------------------------------------------------- /** * Tab component for editing world-map ban-count color thresholds. * * @returns JSX element. */ export function MapTab(): React.JSX.Element { const [thresholds, setThresholds] = useState(null); const [loadError, setLoadError] = useState(null); const load = useCallback(async (): Promise => { try { const data = await fetchMapColorThresholds(); setThresholds(data); } catch (err) { setLoadError( err instanceof ApiError ? err.message : "Failed to load map color thresholds", ); } }, []); useEffect(() => { void load(); }, [load]); if (!thresholds && !loadError) { return (
); } if (loadError) return ( {loadError} ); if (!thresholds) return <>; return ; }