good changes

This commit is contained in:
Andras Schmelczer 2026-03-25 08:04:48 +00:00
parent 160283f1a1
commit c997ea46a5
26 changed files with 991 additions and 288 deletions

View file

@ -69,6 +69,14 @@ const DATA_SOURCES = [
url: 'https://download.geofabrik.de/europe/great-britain-latest.osm.pbf',
license: 'Open Data Commons Open Database License (ODbL)',
},
{
id: 'os-open-greenspace',
name: 'OS Open Greenspace',
origin: 'Ordnance Survey',
use: 'Authoritative green space boundaries for Great Britain, including public parks, gardens, playing fields, and play spaces. Polygon centroids are used for park proximity counts and distance-to-nearest-park calculations.',
url: 'https://osdatahub.os.uk/downloads/open/OpenGreenspace',
license: 'Open Government Licence v3.0',
},
{
id: 'naptan',
name: 'NaPTAN (Public Transport Stops)',
@ -101,14 +109,6 @@ const DATA_SOURCES = [
url: 'https://www.ofcom.org.uk/phones-and-broadband/coverage-and-speeds/connected-nations-20252/data-downloads-2025',
license: 'Open Government Licence v3.0',
},
{
id: 'geosure',
name: 'GeoSure Ground Stability',
origin: 'Ordnance Survey',
use: 'Ground stability hazard ratings on a 5km hex grid covering Great Britain. Six risk categories (collapsible deposits, compressible ground, landslides, running sand, shrink-swell, and soluble rocks) rated Low, Moderate, or Significant. Spatial-joined to postcodes via centroid intersection.',
url: 'https://osdatahub.os.uk/downloads/open/GeoSure',
license: 'Open Government Licence v3.0',
},
{
id: 'council-tax',
name: 'Council Tax Levels 2025-26',

View file

@ -13,6 +13,7 @@ const LOADING_MESSAGES = [
'Analysing your query...',
'Searching for destinations...',
'Generating filters...',
'Refining results...',
];
/** Cycle through loading messages to show progress. */
@ -28,9 +29,11 @@ function useLoadingMessage(loading: boolean): string {
// Advance message every 1.5s
timerRef.current = setTimeout(() => setIndex(1), 1500);
const t2 = setTimeout(() => setIndex(2), 3500);
const t3 = setTimeout(() => setIndex(3), 5500);
return () => {
clearTimeout(timerRef.current);
clearTimeout(t2);
clearTimeout(t3);
};
}, [loading]);

View file

@ -1,5 +1,4 @@
import { useMemo, useState } from 'react';
import { useCollapsibleGroups } from '../../hooks/useCollapsibleGroups';
import type {
FeatureFilters,
FeatureMeta,
@ -38,6 +37,8 @@ interface AreaPaneProps {
filters: FeatureFilters;
onNavigateToSource?: (slug: string, featureName: string) => void;
travelTimeEntries?: TravelTimeEntry[];
isGroupExpanded: (name: string) => boolean;
onToggleGroup: (name: string) => void;
}
export default function AreaPane({
@ -52,11 +53,12 @@ export default function AreaPane({
filters,
onNavigateToSource,
travelTimeEntries,
isGroupExpanded,
onToggleGroup,
}: AreaPaneProps) {
const propertyCount = isPostcode && postcodeData ? postcodeData.properties.count : stats?.count;
const featureGroups = useMemo(() => groupFeaturesByCategory(globalFeatures), [globalFeatures]);
const [infoFeature, setInfoFeature] = useState<FeatureMeta | null>(null);
const [collapsedGroups, toggleGroup] = useCollapsibleGroups();
const numericByName = useMemo(() => {
if (!stats) return new Map();
@ -165,17 +167,17 @@ export default function AreaPane({
) ?? []
);
const isExpanded = !collapsedGroups.has(group.name);
const expanded = isGroupExpanded(group.name);
return (
<div key={group.name}>
<CollapsibleGroupHeader
name={group.name}
expanded={isExpanded}
onToggle={() => toggleGroup(group.name)}
expanded={expanded}
onToggle={() => onToggleGroup(group.name)}
className="px-3 py-2.5 text-sm font-bold text-warm-500 bg-warm-50 dark:bg-warm-900 dark:text-warm-400 sticky top-0 z-10 hover:bg-warm-100 dark:hover:bg-warm-800"
/>
{isExpanded && (
{expanded && (
<div className="px-3 py-2 space-y-3">
{stackedCharts
? stackedCharts.map((chart) => {

View file

@ -54,7 +54,7 @@ export default function FeatureBrowser({
const [search, setSearch] = useState('');
const [infoFeature, setInfoFeature] = useState<FeatureMeta | null>(null);
const [travelInfoMode, setTravelInfoMode] = useState<TransportMode | null>(null);
const [expandedGroups, toggleGroup] = useCollapsibleGroups();
const [isGroupExpanded, toggleGroup] = useCollapsibleGroups(true);
const availableTravelModes = useTravelModes();
useEffect(() => {
@ -106,7 +106,7 @@ export default function FeatureBrowser({
</div>
<div className="md:min-h-0 md:flex-1 md:overflow-y-auto flex flex-col">
{mergedGrouped.map((group) => {
const isExpanded = isSearching || expandedGroups.has(group.name);
const isExpanded = isSearching || isGroupExpanded(group.name);
return (
<div key={group.name} className="shrink-0">
<CollapsibleGroupHeader

View file

@ -22,6 +22,7 @@ import { usePOIData } from '../../hooks/usePOIData';
import { useFilters } from '../../hooks/useFilters';
import { useHexagonSelection } from '../../hooks/useHexagonSelection';
import { usePaneResize } from '../../hooks/usePaneResize';
import { useCollapsibleGroups } from '../../hooks/useCollapsibleGroups';
import { useAiFilters } from '../../hooks/useAiFilters';
import { useUrlSync } from '../../hooks/useUrlSync';
import { useTutorial } from '../../hooks/useTutorial';
@ -274,6 +275,7 @@ export default function MapPage({
}, []);
const pois = usePOIData(mapData.bounds, selectedPOICategories);
const [isAreaGroupExpanded, toggleAreaGroup] = useCollapsibleGroups(true);
useUrlSync(
mapData.currentView,
@ -528,6 +530,8 @@ export default function MapPage({
hexagonLocation={hexagonLocation}
filters={filters}
travelTimeEntries={travelTime.activeEntries}
isGroupExpanded={isAreaGroupExpanded}
onToggleGroup={toggleAreaGroup}
/>
);

View file

@ -25,7 +25,7 @@ export default function POIPane({
onNavigateToSource,
}: POIPaneProps) {
const [searchTerm, setSearchTerm] = useState('');
const [collapsedGroups, toggleCollapse] = useCollapsibleGroups();
const [isGroupExpanded, toggleCollapse] = useCollapsibleGroups();
const [showInfo, setShowInfo] = useState(false);
const allCategories = groups.flatMap((g) => g.categories);
@ -150,7 +150,7 @@ export default function POIPane({
const groupSelected = group.categories.filter((c) => selectedCategories.has(c)).length;
const allInGroupSelected = groupSelected === group.categories.length;
const someInGroupSelected = groupSelected > 0 && !allInGroupSelected;
const isCollapsed = collapsedGroups.has(group.name) && !searchTerm;
const isCollapsed = !isGroupExpanded(group.name) && !searchTerm;
return (
<div key={group.name}>

View file

@ -169,15 +169,6 @@ export default function Header({
Pricing
</a>
)}
{user && (
<a
href={PAGE_PATHS.saved}
className={tabClass('saved')}
onClick={(e) => navLink('saved', e)}
>
Saved
</a>
)}
</nav>
)}
</div>
@ -187,20 +178,6 @@ export default function Header({
{/* Desktop-only dashboard actions */}
{!isMobile && activePage === 'dashboard' && (
<>
{onSaveSearch && (
<button
onClick={onSaveSearch}
disabled={savingSearch}
className="flex items-center gap-1.5 px-3 py-1.5 rounded bg-navy-800 hover:bg-navy-700 transition-colors text-sm disabled:opacity-50 disabled:cursor-wait"
>
{savingSearch ? (
<SpinnerIcon className="w-4 h-4 animate-spin" />
) : (
<BookmarkIcon className="w-4 h-4" />
)}
Save
</button>
)}
<button
onClick={handleShare}
disabled={sharing}
@ -232,8 +209,31 @@ export default function Header({
<DownloadIcon className="w-4 h-4" />
{exporting ? 'Exporting...' : 'Export'}
</button>
{onSaveSearch && (
<button
onClick={onSaveSearch}
disabled={savingSearch}
className="flex items-center gap-1.5 px-3 py-1.5 rounded bg-navy-800 hover:bg-navy-700 transition-colors text-sm disabled:opacity-50 disabled:cursor-wait"
>
{savingSearch ? (
<SpinnerIcon className="w-4 h-4 animate-spin" />
) : (
<BookmarkIcon className="w-4 h-4" />
)}
Save
</button>
)}
</>
)}
{!isMobile && user && (
<a
href={PAGE_PATHS.saved}
className={tabClass('saved')}
onClick={(e) => navLink('saved', e)}
>
Saved
</a>
)}
{/* Desktop-only auth */}
{!isMobile && (

View file

@ -90,28 +90,10 @@ export default function MobileMenu({
mobileNavItem('pricing', 'Pricing')}
{user && mobileNavItem('invites', 'Invite Friends')}
{user && mobileNavItem('account', 'Account')}
{user && mobileNavItem('saved', 'Saved')}
{/* Dashboard actions */}
{activePage === 'dashboard' && (
<div className="mt-3 pt-3 border-t border-navy-700 flex flex-col gap-1">
{onSaveSearch && (
<button
onClick={() => {
onSaveSearch();
onClose();
}}
disabled={savingSearch}
className="w-full flex items-center gap-2 px-4 py-3 text-base text-warm-300 hover:bg-navy-800 hover:text-white rounded disabled:opacity-50"
>
{savingSearch ? (
<SpinnerIcon className="w-5 h-5 animate-spin" />
) : (
<BookmarkIcon className="w-5 h-5" />
)}
Save
</button>
)}
<button
onClick={() => {
onShare();
@ -133,8 +115,27 @@ export default function MobileMenu({
<DownloadIcon className="w-5 h-5" />
{exporting ? 'Exporting...' : 'Export'}
</button>
{onSaveSearch && (
<button
onClick={() => {
onSaveSearch();
onClose();
}}
disabled={savingSearch}
className="w-full flex items-center gap-2 px-4 py-3 text-base text-warm-300 hover:bg-navy-800 hover:text-white rounded disabled:opacity-50"
>
{savingSearch ? (
<SpinnerIcon className="w-5 h-5 animate-spin" />
) : (
<BookmarkIcon className="w-5 h-5" />
)}
Save
</button>
)}
{user && mobileNavItem('saved', 'Saved')}
</div>
)}
{activePage !== 'dashboard' && user && mobileNavItem('saved', 'Saved')}
</nav>
{/* Theme toggle + Auth section at bottom */}