Add one change & crime

This commit is contained in:
Andras Schmelczer 2026-06-14 14:51:29 +01:00
parent 8e4c56bb0d
commit 54fbcb1ea6
17 changed files with 306 additions and 154 deletions

View file

@ -92,6 +92,7 @@ interface FiltersProps {
onTravelTimeDragEnd: (index: number) => void;
onTravelTimeToggleBest: (index: number) => void;
onTravelTimeToggleNoChange: (index: number) => void;
onTravelTimeToggleOneChange: (index: number) => void;
onTravelTimeToggleNoBuses: (index: number) => void;
aiFilterLoading: boolean;
aiFilterError: string | null;
@ -139,6 +140,7 @@ export default memo(function Filters({
onTravelTimeDragEnd,
onTravelTimeToggleBest,
onTravelTimeToggleNoChange,
onTravelTimeToggleOneChange,
onTravelTimeToggleNoBuses,
aiFilterLoading,
aiFilterError,
@ -667,6 +669,7 @@ export default memo(function Filters({
onTravelTimeDragEnd={onTravelTimeDragEnd}
onTravelTimeToggleBest={onTravelTimeToggleBest}
onTravelTimeToggleNoChange={onTravelTimeToggleNoChange}
onTravelTimeToggleOneChange={onTravelTimeToggleOneChange}
onTravelTimeToggleNoBuses={onTravelTimeToggleNoBuses}
/>

View file

@ -181,6 +181,7 @@ export default function MapPage({
handleTimeRangeChange,
handleToggleBest,
handleToggleNoChange,
handleToggleOneChange,
handleToggleNoBuses,
} = useTravelTime(initialTravelTime);
@ -248,6 +249,7 @@ export default function MapPage({
representable.map(({ tt, parsed }) => ({
mode: parsed.mode,
noChange: parsed.noChange,
oneChange: parsed.oneChange,
noBuses: parsed.noBuses,
slug: tt.slug,
label: tt.label,
@ -800,6 +802,7 @@ export default function MapPage({
onTravelTimeDragEnd={handleTravelTimeDragEnd}
onTravelTimeToggleBest={handleToggleBest}
onTravelTimeToggleNoChange={handleToggleNoChange}
onTravelTimeToggleOneChange={handleToggleOneChange}
onTravelTimeToggleNoBuses={handleToggleNoBuses}
aiFilterLoading={aiFilterLoading}
aiFilterError={aiFilterError}
@ -853,6 +856,7 @@ export default function MapPage({
handleToggleBest,
handleToggleNoBuses,
handleToggleNoChange,
handleToggleOneChange,
handleTogglePin,
handleTravelTimeDragEnd,
handleTravelTimeRemoveEntry,

View file

@ -27,6 +27,7 @@ interface TravelTimeCardProps {
timeRange: [number, number] | null;
useBest: boolean;
noChange: boolean;
oneChange: boolean;
noBuses: boolean;
isPinned: boolean;
isActive: boolean;
@ -39,6 +40,7 @@ interface TravelTimeCardProps {
onDragEnd: () => void;
onToggleBest: () => void;
onToggleNoChange: () => void;
onToggleOneChange: () => void;
onToggleNoBuses: () => void;
onRemove: () => void;
filterImpact?: number;
@ -52,6 +54,7 @@ export function TravelTimeCard({
timeRange,
useBest,
noChange,
oneChange,
noBuses,
isPinned,
isActive,
@ -64,6 +67,7 @@ export function TravelTimeCard({
onDragEnd,
onToggleBest,
onToggleNoChange,
onToggleOneChange,
onToggleNoBuses,
onRemove,
filterImpact,
@ -75,6 +79,7 @@ export function TravelTimeCard({
const [showInfo, setShowInfo] = useState(false);
const [showBestInfo, setShowBestInfo] = useState(false);
const [showNoChangeInfo, setShowNoChangeInfo] = useState(false);
const [showOneChangeInfo, setShowOneChangeInfo] = useState(false);
const [showNoBusesInfo, setShowNoBusesInfo] = useState(false);
const handleDestinationSelect = useCallback(
@ -169,6 +174,20 @@ export function TravelTimeCard({
<InfoIcon className="w-3 h-3" />
</IconButton>
</div>
<div className="flex items-center gap-0.5">
<PillToggle
label={t('travel.oneChange')}
active={oneChange}
onClick={onToggleOneChange}
size="xs"
/>
<IconButton
onClick={() => setShowOneChangeInfo(true)}
title={t('travel.oneChangeTitle')}
>
<InfoIcon className="w-3 h-3" />
</IconButton>
</div>
<div className="flex items-center gap-0.5">
<PillToggle
label={t('travel.noBuses')}
@ -201,6 +220,14 @@ export function TravelTimeCard({
</InfoPopup>
)}
{showOneChangeInfo && (
<InfoPopup title={t('travel.oneChangeTitle')} onClose={() => setShowOneChangeInfo(false)}>
<p className="text-sm text-warm-700 dark:text-warm-300 leading-relaxed">
<Trans i18nKey="travel.oneChangeDesc" components={{ strong: <strong /> }} />
</p>
</InfoPopup>
)}
{showNoBusesInfo && (
<InfoPopup title={t('travel.noBusesTitle')} onClose={() => setShowNoBusesInfo(false)}>
<p className="text-sm text-warm-700 dark:text-warm-300 leading-relaxed">

View file

@ -59,6 +59,7 @@ interface ActiveFilterListProps {
onTravelTimeDragEnd: (index: number) => void;
onTravelTimeToggleBest: (index: number) => void;
onTravelTimeToggleNoChange: (index: number) => void;
onTravelTimeToggleOneChange: (index: number) => void;
onTravelTimeToggleNoBuses: (index: number) => void;
}
@ -88,6 +89,7 @@ export function ActiveFilterList({
onTravelTimeDragEnd,
onTravelTimeToggleBest,
onTravelTimeToggleNoChange,
onTravelTimeToggleOneChange,
onTravelTimeToggleNoBuses,
}: ActiveFilterListProps) {
const travelCards = (
@ -105,6 +107,7 @@ export function ActiveFilterList({
onTravelTimeDragEnd={onTravelTimeDragEnd}
onTravelTimeToggleBest={onTravelTimeToggleBest}
onTravelTimeToggleNoChange={onTravelTimeToggleNoChange}
onTravelTimeToggleOneChange={onTravelTimeToggleOneChange}
onTravelTimeToggleNoBuses={onTravelTimeToggleNoBuses}
onDragStart={onDragStart}
onDragChange={onDragChange}

View file

@ -57,6 +57,7 @@ interface ActiveFiltersPanelProps {
onTravelTimeDragEnd: (index: number) => void;
onTravelTimeToggleBest: (index: number) => void;
onTravelTimeToggleNoChange: (index: number) => void;
onTravelTimeToggleOneChange: (index: number) => void;
onTravelTimeToggleNoBuses: (index: number) => void;
}
@ -102,6 +103,7 @@ export function ActiveFiltersPanel({
onTravelTimeDragEnd,
onTravelTimeToggleBest,
onTravelTimeToggleNoChange,
onTravelTimeToggleOneChange,
onTravelTimeToggleNoBuses,
}: ActiveFiltersPanelProps) {
const { t } = useTranslation();
@ -206,6 +208,7 @@ export function ActiveFiltersPanel({
onTravelTimeDragEnd={onTravelTimeDragEnd}
onTravelTimeToggleBest={onTravelTimeToggleBest}
onTravelTimeToggleNoChange={onTravelTimeToggleNoChange}
onTravelTimeToggleOneChange={onTravelTimeToggleOneChange}
onTravelTimeToggleNoBuses={onTravelTimeToggleNoBuses}
/>
</div>

View file

@ -21,6 +21,7 @@ interface TravelTimeFilterCardsProps {
onTravelTimeDragEnd: (index: number) => void;
onTravelTimeToggleBest: (index: number) => void;
onTravelTimeToggleNoChange: (index: number) => void;
onTravelTimeToggleOneChange: (index: number) => void;
onTravelTimeToggleNoBuses: (index: number) => void;
onDragStart: (name: string, initialValue?: [number, number]) => void;
onDragChange: (value: [number, number]) => void;
@ -40,6 +41,7 @@ export function TravelTimeFilterCards({
onTravelTimeDragEnd,
onTravelTimeToggleBest,
onTravelTimeToggleNoChange,
onTravelTimeToggleOneChange,
onTravelTimeToggleNoBuses,
onDragStart,
onDragChange,
@ -57,6 +59,7 @@ export function TravelTimeFilterCards({
timeRange={entry.timeRange}
useBest={entry.useBest}
noChange={entry.noChange ?? false}
oneChange={entry.oneChange ?? false}
noBuses={entry.noBuses ?? false}
isPinned={pinnedFeature === fieldKey}
isActive={activeFeature === fieldKey}
@ -71,6 +74,7 @@ export function TravelTimeFilterCards({
onDragEnd={() => onTravelTimeDragEnd(index)}
onToggleBest={() => onTravelTimeToggleBest(index)}
onToggleNoChange={() => onTravelTimeToggleNoChange(index)}
onToggleOneChange={() => onTravelTimeToggleOneChange(index)}
onToggleNoBuses={() => onTravelTimeToggleNoBuses(index)}
onRemove={() => onTravelTimeRemoveEntry(index)}
filterImpact={filterImpacts?.[fieldKey]}

View file

@ -150,6 +150,12 @@ export function useExportController({
const params = new URLSearchParams();
if (isListMode) {
params.set('postcodes', postcodeList.join(','));
// Pass the active filters so the export keeps its two sheets — "Selected"
// (the filtered feature columns) and "All Data" — even in list mode. The
// server uses them only to pick columns; the supplied postcodes still
// drive which rows appear.
const filterStr = buildFilterString(filters, features);
if (filterStr) params.set('filters', filterStr);
if (shareCode) params.set('share', shareCode);
} else {
const { south, west, north, east } = bounds!;