Let temperature formatting ship without dashboard bulk
Temperature formatting was embedded in the large dashboard utility module, so small scan-terminal views had to import the heavy utility surface for simple labels. This moves the pure temperature helpers into a lightweight module while keeping dashboard-utils re-exports for compatibility. Constraint: Preserve existing temperature text output and all dashboard-utils import compatibility. Rejected: Split chart, pace, and model helpers in the same pass | those helpers have wider coupling and should move one boundary at a time. Confidence: high Scope-risk: narrow Reversibility: clean Tested: TypeScript diagnostics for temperature-utils, dashboard-utils, and OpportunityTable Tested: npm run build Not-tested: Bundle analyzer size comparison.
This commit is contained in:
@@ -9,13 +9,15 @@ import type {
|
|||||||
} from "@/lib/dashboard-types";
|
} from "@/lib/dashboard-types";
|
||||||
import { getLocalizedCityName } from "@/lib/dashboard-home-copy";
|
import { getLocalizedCityName } from "@/lib/dashboard-home-copy";
|
||||||
import {
|
import {
|
||||||
formatTemperatureValue,
|
|
||||||
getModelView,
|
getModelView,
|
||||||
getProbabilityView,
|
getProbabilityView,
|
||||||
getTodayPaceView,
|
getTodayPaceView,
|
||||||
|
} from "@/lib/dashboard-utils";
|
||||||
|
import {
|
||||||
|
formatTemperatureValue,
|
||||||
normalizeTemperatureLabel,
|
normalizeTemperatureLabel,
|
||||||
normalizeTemperatureSymbol,
|
normalizeTemperatureSymbol,
|
||||||
} from "@/lib/dashboard-utils";
|
} from "@/lib/temperature-utils";
|
||||||
|
|
||||||
type PhaseMeta = {
|
type PhaseMeta = {
|
||||||
label: string;
|
label: string;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { CityDetail, ScanOpportunityRow } from "@/lib/dashboard-types";
|
import type { CityDetail, ScanOpportunityRow } from "@/lib/dashboard-types";
|
||||||
import { formatTemperatureValue, getTodayPaceView } from "@/lib/dashboard-utils";
|
import { getTodayPaceView } from "@/lib/dashboard-utils";
|
||||||
|
import { formatTemperatureValue } from "@/lib/temperature-utils";
|
||||||
import { getPeakWindowLabel } from "@/components/dashboard/scan-terminal/decision-utils";
|
import { getPeakWindowLabel } from "@/components/dashboard/scan-terminal/decision-utils";
|
||||||
|
|
||||||
export function AiForecastKPIBar({
|
export function AiForecastKPIBar({
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ import {
|
|||||||
useCityMarketScan,
|
useCityMarketScan,
|
||||||
} from "@/components/dashboard/scan-terminal/use-ai-city-card-data";
|
} from "@/components/dashboard/scan-terminal/use-ai-city-card-data";
|
||||||
import type { CityDetail, ScanOpportunityRow } from "@/lib/dashboard-types";
|
import type { CityDetail, ScanOpportunityRow } from "@/lib/dashboard-types";
|
||||||
import { formatTemperatureValue, getModelView, getTodayPaceView } from "@/lib/dashboard-utils";
|
import { getModelView, getTodayPaceView } from "@/lib/dashboard-utils";
|
||||||
|
import { formatTemperatureValue } from "@/lib/temperature-utils";
|
||||||
|
|
||||||
function toFiniteDecisionNumber(value: unknown) {
|
function toFiniteDecisionNumber(value: unknown) {
|
||||||
if (value == null || value === "") return null;
|
if (value == null || value === "") return null;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { memo, useEffect, useMemo, useState } from "react";
|
|||||||
import { getWindowPhaseMeta } from "@/components/dashboard/OpportunityTable";
|
import { getWindowPhaseMeta } from "@/components/dashboard/OpportunityTable";
|
||||||
import type { ScanOpportunityRow } from "@/lib/dashboard-types";
|
import type { ScanOpportunityRow } from "@/lib/dashboard-types";
|
||||||
import { getLocalizedCityName } from "@/lib/dashboard-home-copy";
|
import { getLocalizedCityName } from "@/lib/dashboard-home-copy";
|
||||||
import { formatTemperatureValue } from "@/lib/dashboard-utils";
|
import { formatTemperatureValue } from "@/lib/temperature-utils";
|
||||||
import {
|
import {
|
||||||
formatShortDate,
|
formatShortDate,
|
||||||
getPeakCountdownMeta,
|
getPeakCountdownMeta,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { memo, useMemo } from "react";
|
import { memo, useMemo } from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import type { ScanOpportunityRow, ScanTerminalResponse } from "@/lib/dashboard-types";
|
import type { ScanOpportunityRow, ScanTerminalResponse } from "@/lib/dashboard-types";
|
||||||
import { formatTemperatureValue } from "@/lib/dashboard-utils";
|
import { formatTemperatureValue } from "@/lib/temperature-utils";
|
||||||
import { LoadingSignal } from "@/components/dashboard/scan-terminal/LoadingSignal";
|
import { LoadingSignal } from "@/components/dashboard/scan-terminal/LoadingSignal";
|
||||||
import {
|
import {
|
||||||
formatRowPrice,
|
formatRowPrice,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { MarketScan, MarketTopBucket } from "@/lib/dashboard-types";
|
import type { MarketScan, MarketTopBucket } from "@/lib/dashboard-types";
|
||||||
|
import { getTodayPaceView } from "@/lib/dashboard-utils";
|
||||||
import {
|
import {
|
||||||
formatTemperatureValue,
|
formatTemperatureValue,
|
||||||
getTodayPaceView,
|
|
||||||
normalizeTemperatureLabel,
|
normalizeTemperatureLabel,
|
||||||
} from "@/lib/dashboard-utils";
|
} from "@/lib/temperature-utils";
|
||||||
import type { AiCityForecastPayload } from "@/components/dashboard/scan-terminal/types";
|
import type { AiCityForecastPayload } from "@/components/dashboard/scan-terminal/types";
|
||||||
|
|
||||||
export type WeatherDecisionView = {
|
export type WeatherDecisionView = {
|
||||||
|
|||||||
@@ -6,6 +6,17 @@ import {
|
|||||||
NearbyStation,
|
NearbyStation,
|
||||||
} from "@/lib/dashboard-types";
|
} from "@/lib/dashboard-types";
|
||||||
import { normalizeObservationSourceLabel } from "@/lib/source-labels";
|
import { normalizeObservationSourceLabel } from "@/lib/source-labels";
|
||||||
|
import {
|
||||||
|
formatTemperatureValue,
|
||||||
|
normalizeTemperatureLabel,
|
||||||
|
normalizeTemperatureSymbol,
|
||||||
|
} from "@/lib/temperature-utils";
|
||||||
|
|
||||||
|
export {
|
||||||
|
formatTemperatureValue,
|
||||||
|
normalizeTemperatureLabel,
|
||||||
|
normalizeTemperatureSymbol,
|
||||||
|
} from "@/lib/temperature-utils";
|
||||||
|
|
||||||
const METAR_WX_MAP: Record<
|
const METAR_WX_MAP: Record<
|
||||||
string,
|
string,
|
||||||
@@ -45,42 +56,6 @@ function containsCjk(text: string) {
|
|||||||
return /[\u3400-\u9fff]/.test(text);
|
return /[\u3400-\u9fff]/.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeTemperatureSymbol(value?: string | null) {
|
|
||||||
return String(value || "").toUpperCase().includes("F") ? "°F" : "°C";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatTemperatureValue(
|
|
||||||
value: number | null | undefined,
|
|
||||||
symbol?: string | null,
|
|
||||||
options?: { signed?: boolean; digits?: number },
|
|
||||||
) {
|
|
||||||
const numeric = Number(value);
|
|
||||||
if (!Number.isFinite(numeric)) return "--";
|
|
||||||
const digits = options?.digits ?? 0;
|
|
||||||
const sign = options?.signed && numeric >= 0 ? "+" : "";
|
|
||||||
return `${sign}${numeric.toFixed(digits)}${normalizeTemperatureSymbol(symbol)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeTemperatureLabel(
|
|
||||||
label?: string | null,
|
|
||||||
fallbackSymbol?: string | null,
|
|
||||||
) {
|
|
||||||
const raw = String(label || "").trim();
|
|
||||||
if (!raw) return "";
|
|
||||||
const normalizedSymbol = normalizeTemperatureSymbol(fallbackSymbol);
|
|
||||||
let next = raw.replace(/℃/gi, "°C");
|
|
||||||
next = next.replace(/°?([CF])\b/gi, (_, unit: string) => `°${unit.toUpperCase()}`);
|
|
||||||
if (!/[°][CF]/.test(next) && /\d/.test(next)) {
|
|
||||||
next = `${next}${normalizedSymbol}`;
|
|
||||||
}
|
|
||||||
if (normalizedSymbol === "°F") {
|
|
||||||
next = next.replace(/°C/g, "°F");
|
|
||||||
} else {
|
|
||||||
next = next.replace(/°F/g, "°C");
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLocalizedDynamicCommentary(
|
function getLocalizedDynamicCommentary(
|
||||||
detail: CityDetail,
|
detail: CityDetail,
|
||||||
locale: Locale,
|
locale: Locale,
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
export function normalizeTemperatureSymbol(value?: string | null) {
|
||||||
|
return String(value || "").toUpperCase().includes("F") ? "°F" : "°C";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatTemperatureValue(
|
||||||
|
value: number | null | undefined,
|
||||||
|
symbol?: string | null,
|
||||||
|
options?: { signed?: boolean; digits?: number },
|
||||||
|
) {
|
||||||
|
const numeric = Number(value);
|
||||||
|
if (!Number.isFinite(numeric)) return "--";
|
||||||
|
const digits = options?.digits ?? 0;
|
||||||
|
const sign = options?.signed && numeric >= 0 ? "+" : "";
|
||||||
|
return `${sign}${numeric.toFixed(digits)}${normalizeTemperatureSymbol(symbol)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeTemperatureLabel(
|
||||||
|
label?: string | null,
|
||||||
|
fallbackSymbol?: string | null,
|
||||||
|
) {
|
||||||
|
const raw = String(label || "").trim();
|
||||||
|
if (!raw) return "";
|
||||||
|
const normalizedSymbol = normalizeTemperatureSymbol(fallbackSymbol);
|
||||||
|
let next = raw.replace(/℃/gi, "°C");
|
||||||
|
next = next.replace(/°?([CF])\b/gi, (_, unit: string) => `°${unit.toUpperCase()}`);
|
||||||
|
if (!/[°][CF]/.test(next) && /\d/.test(next)) {
|
||||||
|
next = `${next}${normalizedSymbol}`;
|
||||||
|
}
|
||||||
|
if (normalizedSymbol === "°F") {
|
||||||
|
next = next.replace(/°C/g, "°F");
|
||||||
|
} else {
|
||||||
|
next = next.replace(/°F/g, "°C");
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user