refactor: extract shared utility functions to eliminate duplication

This commit is contained in:
Viktor Barzin 2026-02-28 16:02:06 +00:00
parent b720013a08
commit 1037ff164d
No known key found for this signature in database
GPG key ID: 0EB088298288D958
10 changed files with 85 additions and 110 deletions

View file

@ -2,12 +2,7 @@ import { ExternalLink, Heart, X, Bed, Maximize2, PoundSterling, Building, Clock,
import { Button } from './ui/button';
import { PhotoCarousel } from './PhotoCarousel';
import type { ListingDetailData, DecisionType, POIDistanceInfo } from '@/types';
function formatDate(value: string): string {
const date = new Date(value);
if (isNaN(date.getTime())) return value;
return date.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
}
import { formatDate, formatDuration } from '@/utils/format';
interface ListingDetailProps {
detail: ListingDetailData;
@ -15,14 +10,6 @@ interface ListingDetailProps {
onClearDecision: () => void;
}
function formatDuration(seconds: number): string {
const minutes = Math.round(seconds / 60);
if (minutes < 60) return `${minutes}m`;
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return mins > 0 ? `${hours}h${mins}m` : `${hours}h`;
}
function TravelModeIcon({ mode }: { mode: string }) {
switch (mode) {
case 'WALK': return <Footprints className="h-3 w-3" />;

View file

@ -4,6 +4,7 @@ import { MapPin, PoundSterling } from 'lucide-react';
import { SwipeableCardRow } from './SwipeableCardRow';
import { ListView } from './ListView';
import type { GeoJSONFeatureCollection, PropertyProperties, PropertyFeature } from '@/types';
import { formatCurrency } from '@/utils/format';
interface MobileBottomSheetProps {
listingData: GeoJSONFeatureCollection | null;
@ -14,11 +15,6 @@ interface MobileBottomSheetProps {
onSnapChange?: (snap: string | number | null) => void;
}
function formatCurrency(value: number): string {
if (value >= 1000) return `£${(value / 1000).toFixed(1)}k`;
return `£${Math.round(value)}`;
}
export function MobileBottomSheet({
listingData,
onPropertyClick,

View file

@ -3,20 +3,7 @@ import useEmblaCarousel from 'embla-carousel-react';
import { ExternalLink, Bed, Maximize2, PoundSterling, Clock, Building, Footprints, Bike, Train } from 'lucide-react';
import { Button } from './ui/button';
import type { PropertyProperties, POIDistanceInfo, POI } from '@/types';
function formatDate(value: string): string {
const date = new Date(value);
if (isNaN(date.getTime())) return value;
return date.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
}
function formatDuration(seconds: number): string {
const minutes = Math.round(seconds / 60);
if (minutes < 60) return `${minutes}m`;
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return mins > 0 ? `${hours}h${mins}m` : `${hours}h`;
}
import { formatDate, formatDuration } from '@/utils/format';
function TravelModeIcon({ mode }: { mode: string }) {
switch (mode) {

View file

@ -1,6 +1,7 @@
import { BarChart3, MapPin, PoundSterling, Maximize2, List, Map as MapIcon, Heart } from 'lucide-react';
import { Button } from './ui/button';
import type { GeoJSONFeatureCollection, PropertyFeature } from '@/types';
import { formatCurrency } from '@/utils/format';
export type ViewMode = 'map' | 'list' | 'split' | 'saved';
@ -53,13 +54,6 @@ function calculateStats(data: GeoJSONFeatureCollection | null): ListingStats {
return { count, avgPrice, avgPricePerSqm, avgSize };
}
function formatCurrency(value: number): string {
if (value >= 1000) {
return `£${(value / 1000).toFixed(1)}k`;
}
return `£${Math.round(value)}`;
}
export function StatsBar({ listingData, viewMode, onViewModeChange, likedCount = 0 }: StatsBarProps) {
const stats = calculateStats(listingData);

View file

@ -4,6 +4,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/t
import { Button } from './ui/button';
import { Loader2, CheckCircle2, XCircle, X, Trash2 } from 'lucide-react';
import { TaskProgressDrawer } from './TaskProgressDrawer';
import { isTerminalStatus, taskStateToResult } from '@/utils/taskUtils';
interface TaskIndicatorProps {
tasks: Record<string, TaskState>;
@ -14,37 +15,6 @@ interface TaskIndicatorProps {
onTaskCompleted?: () => void;
}
/** Convert a TaskState into a TaskResult (for the drawer). */
function taskStateToResult(ts: TaskState): TaskResult {
return {
progress: ts.progress ?? 0,
processed: ts.processed,
total: ts.total,
phase: ts.phase,
message: ts.message,
subqueries_probed: ts.subqueries_probed,
subqueries_initial: ts.subqueries_initial,
estimated_results: ts.estimated_results,
subqueries_total: ts.subqueries_total,
subqueries_completed: ts.subqueries_completed,
ids_collected: ts.ids_collected,
pages_fetched: ts.pages_fetched,
fetching_done: ts.fetching_done,
details_fetched: ts.details_fetched,
images_downloaded: ts.images_downloaded,
ocr_completed: ts.ocr_completed,
failed: ts.failed,
elapsed_seconds: ts.elapsed_seconds,
rate_per_second: ts.rate_per_second,
eta_seconds: ts.eta_seconds,
logs: ts.logs,
};
}
function isTerminalStatus(status: string): boolean {
return status === 'SUCCESS' || status === 'FAILURE' || status === 'REVOKED';
}
export function TaskIndicator({
tasks,
activeTaskId,

View file

@ -11,6 +11,7 @@ import { Button } from './ui/button';
import { CheckCircle2, Circle, Loader2, XCircle, MapPin, Search } from 'lucide-react';
import { useEffect, useRef, useMemo } from 'react';
import { useIsMobile } from '@/hooks/use-mobile';
import { isTerminalStatus, taskStateToResult } from '@/utils/taskUtils';
interface TaskProgressDrawerProps {
open: boolean;
@ -62,10 +63,6 @@ function taskTypeIcon(type: 'scrape' | 'poi' | 'task') {
}
}
function isTerminalStatus(status: string): boolean {
return status === 'SUCCESS' || status === 'FAILURE' || status === 'REVOKED';
}
function getPhaseIndex(phase: TaskPhase | undefined): number {
if (!phase) return -1;
if (phase === 'splitting_complete') return 1;
@ -324,33 +321,6 @@ function LogViewer({ logs }: { logs: string[] }) {
);
}
/** Convert TaskState → TaskResult for existing phase detail components. */
function taskStateToResult(ts: TaskState): TaskResult {
return {
progress: ts.progress ?? 0,
processed: ts.processed,
total: ts.total,
phase: ts.phase,
message: ts.message,
subqueries_probed: ts.subqueries_probed,
subqueries_initial: ts.subqueries_initial,
estimated_results: ts.estimated_results,
subqueries_total: ts.subqueries_total,
subqueries_completed: ts.subqueries_completed,
ids_collected: ts.ids_collected,
pages_fetched: ts.pages_fetched,
fetching_done: ts.fetching_done,
details_fetched: ts.details_fetched,
images_downloaded: ts.images_downloaded,
ocr_completed: ts.ocr_completed,
failed: ts.failed,
elapsed_seconds: ts.elapsed_seconds,
rate_per_second: ts.rate_per_second,
eta_seconds: ts.eta_seconds,
logs: ts.logs,
};
}
function TaskTabBar({
tasks,
selectedTaskId,