Refactor and other improvements
This commit is contained in:
parent
04a78e7bfe
commit
6c90cf3c0f
47 changed files with 2705 additions and 1568 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
import { useCollapsibleGroups } from '../../hooks/useCollapsibleGroups';
|
||||
import type {
|
||||
FeatureFilters,
|
||||
FeatureMeta,
|
||||
|
|
@ -15,13 +16,15 @@ import StackedBarChart from './StackedBarChart';
|
|||
import StackedEnumChart from './StackedEnumChart';
|
||||
import PriceHistoryChart from './PriceHistoryChart';
|
||||
import ExternalSearchLinks from './ExternalSearchLinks';
|
||||
import { InfoIcon, CloseIcon, ChevronIcon } from '../ui/icons';
|
||||
import { InfoIcon, CloseIcon } from '../ui/icons';
|
||||
import { CollapsibleGroupHeader } from '../ui/CollapsibleGroupHeader';
|
||||
import { LightbulbIcon } from '../ui/icons/LightbulbIcon';
|
||||
import { IconButton } from '../ui/IconButton';
|
||||
import { FeatureInfoPopup } from '../ui/FeatureInfoPopup';
|
||||
import { EmptyState } from '../ui/EmptyState';
|
||||
import { FeatureLabel } from '../ui/FeatureLabel';
|
||||
import AISummaryCard from './AISummaryCard';
|
||||
import StreetViewEmbed from './StreetViewEmbed';
|
||||
import HistogramLegend from './HistogramLegend';
|
||||
|
||||
interface AreaPaneProps {
|
||||
stats: HexagonStatsResponse | null;
|
||||
|
|
@ -60,17 +63,9 @@ export default function AreaPane({
|
|||
const propertyCount = isPostcode && postcodeData ? postcodeData.properties.count : stats?.count;
|
||||
const featureGroups = useMemo(() => groupFeaturesByCategory(globalFeatures), [globalFeatures]);
|
||||
const [infoFeature, setInfoFeature] = useState<FeatureMeta | null>(null);
|
||||
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(new Set());
|
||||
const [collapsedGroups, toggleGroup] = useCollapsibleGroups();
|
||||
const [aiSummaryExpanded, setAiSummaryExpanded] = useState(true);
|
||||
|
||||
const toggleGroup = (name: string) =>
|
||||
setCollapsedGroups((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(name)) next.delete(name);
|
||||
else next.add(name);
|
||||
return next;
|
||||
});
|
||||
|
||||
const numericByName = useMemo(() => {
|
||||
if (!stats) return new Map();
|
||||
return new Map(stats.numeric_features.map((feature) => [feature.name, feature]));
|
||||
|
|
@ -138,78 +133,18 @@ export default function AreaPane({
|
|||
)}
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{/* AI Summary Card */}
|
||||
{(aiSummary || aiSummaryLoading || aiSummaryError) && (
|
||||
<div className="px-3 pt-3 pb-1">
|
||||
<div className="bg-teal-50 dark:bg-teal-900/20 border border-teal-200 dark:border-teal-800/50 rounded p-2.5">
|
||||
<button
|
||||
onClick={() => setAiSummaryExpanded(!aiSummaryExpanded)}
|
||||
className="w-full flex items-center justify-between gap-1.5 mb-1.5"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<LightbulbIcon className="w-3.5 h-3.5 text-teal-600 dark:text-teal-400" />
|
||||
<span className="text-xs font-semibold text-teal-700 dark:text-teal-400">
|
||||
AI Summary
|
||||
</span>
|
||||
</div>
|
||||
<ChevronIcon
|
||||
direction={aiSummaryExpanded ? 'down' : 'right'}
|
||||
className="w-3.5 h-3.5 text-teal-600 dark:text-teal-400"
|
||||
/>
|
||||
</button>
|
||||
{aiSummaryExpanded && (
|
||||
<>
|
||||
{aiSummaryError ? (
|
||||
<div className="text-xs text-warm-600 dark:text-warm-400">
|
||||
Failed to generate summary.
|
||||
</div>
|
||||
) : aiSummaryLoading ? (
|
||||
<div className="space-y-1.5">
|
||||
<div className="h-3 bg-teal-200/60 dark:bg-teal-800/40 rounded animate-pulse w-full" />
|
||||
<div className="h-3 bg-teal-200/60 dark:bg-teal-800/40 rounded animate-pulse w-4/5" />
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-warm-700 dark:text-warm-300 leading-relaxed">
|
||||
{aiSummary}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<AISummaryCard
|
||||
summary={aiSummary}
|
||||
loading={aiSummaryLoading}
|
||||
error={aiSummaryError}
|
||||
expanded={aiSummaryExpanded}
|
||||
onToggleExpanded={() => setAiSummaryExpanded(!aiSummaryExpanded)}
|
||||
/>
|
||||
{loading && !stats ? (
|
||||
<LoadingSkeleton />
|
||||
) : stats ? (
|
||||
<div>
|
||||
{/* Histogram color legend */}
|
||||
<div className="mx-3 mt-3 bg-teal-50 dark:bg-teal-900/20 border border-teal-200 dark:border-teal-800/50 rounded p-2.5 text-xs">
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 bg-teal-500 dark:bg-teal-400 rounded" />
|
||||
<span className="text-warm-700 dark:text-warm-300">
|
||||
<span className="font-medium text-warm-900 dark:text-warm-100">Teal bars</span>{' '}
|
||||
show the distribution in this selected area
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 bg-warm-300/60 dark:bg-warm-600/60 rounded" />
|
||||
<span className="text-warm-700 dark:text-warm-300">
|
||||
<span className="font-medium text-warm-900 dark:text-warm-100">Gray bars</span>{' '}
|
||||
show the overall distribution across all areas
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-px border-t border-dashed border-warm-500 dark:border-warm-400" />
|
||||
<span className="text-warm-700 dark:text-warm-300">
|
||||
<span className="font-medium text-warm-900 dark:text-warm-100">
|
||||
Dashed line
|
||||
</span>{' '}
|
||||
indicates the global average
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<HistogramLegend />
|
||||
{featureGroups.map((group) => {
|
||||
const hasData = group.features.some(
|
||||
(feature) => numericByName.has(feature.name) || enumByName.has(feature.name)
|
||||
|
|
@ -460,25 +395,7 @@ export default function AreaPane({
|
|||
</div>
|
||||
);
|
||||
})}
|
||||
{/* Google Street View */}
|
||||
{hexagonLocation && (
|
||||
<div>
|
||||
<div className="px-3 py-1.5 text-xs font-bold text-warm-500 bg-warm-50 dark:bg-warm-900 dark:text-warm-400 sticky top-0">
|
||||
Street View
|
||||
</div>
|
||||
<div className="px-3 py-2">
|
||||
<div className="rounded overflow-hidden border border-warm-200 dark:border-warm-700">
|
||||
<iframe
|
||||
className="w-full"
|
||||
style={{ height: 240, border: 0 }}
|
||||
loading="lazy"
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
src={`https://maps.google.com/maps?layer=c&cbll=${hexagonLocation.lat},${hexagonLocation.lon}&cbp=11,0,0,0,0&output=svembed`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{hexagonLocation && <StreetViewEmbed location={hexagonLocation} />}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue