Bladeren bron

feat: historical temperature curve navigation + add 8 new city aliases

Add date picker (prev/next day arrows) to HKO daily temperature curve,
with API support for ?date=YYYY-MM-DD query param. Add missing city
aliases for Wellington, Milan, Lucknow, Singapore, Ankara, Warsaw,
Madrid, and Taipei so signals can be generated for newly listed markets.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Horace Zhang 7 maanden geleden
bovenliggende
commit
918a8cc37c

+ 8 - 0
packages/core/src/constants.ts

@@ -62,6 +62,14 @@ export const CITY_ALIASES: Record<string, string> = {
   'Sao Paulo': 'sao_paulo',
   'São Paulo': 'sao_paulo',
   'Shanghai': 'shanghai',
+  'Wellington': 'wellington',
+  'Milan': 'milan',
+  'Lucknow': 'lucknow',
+  'Singapore': 'singapore',
+  'Ankara': 'ankara',
+  'Warsaw': 'warsaw',
+  'Madrid': 'madrid',
+  'Taipei': 'taipei',
 };
 
 // ─── API URLs ────────────────────────────────────────────────────────────────

+ 14 - 7
packages/dashboard/src/app/api/hko-daily-readings/route.ts

@@ -1,19 +1,26 @@
-import { NextResponse } from 'next/server';
+import { NextRequest, NextResponse } from 'next/server';
 import { getObservedDailyReadings } from '@/lib/queries';
 
 export const dynamic = 'force-dynamic';
 
 /**
- * GET /api/hko-daily-readings — get all observed HKO temperature readings for today.
+ * GET /api/hko-daily-readings — get observed HKO temperature readings.
+ * Optional ?date=YYYY-MM-DD param; defaults to today (HKT).
  * Returns array of { temp, time } sorted chronologically.
  */
-export async function GET() {
+export async function GET(request: NextRequest) {
   try {
-    // Use Hong Kong timezone to determine "today"
-    const nowHKT = new Date().toLocaleDateString('en-CA', { timeZone: 'Asia/Hong_Kong' });
-    const todayDate = new Date(nowHKT + 'T00:00:00+08:00');
+    const dateParam = request.nextUrl.searchParams.get('date');
+    let targetDate: Date;
 
-    const readings = await getObservedDailyReadings('hong_kong', todayDate);
+    if (dateParam && /^\d{4}-\d{2}-\d{2}$/.test(dateParam)) {
+      targetDate = new Date(dateParam + 'T00:00:00+08:00');
+    } else {
+      const nowHKT = new Date().toLocaleDateString('en-CA', { timeZone: 'Asia/Hong_Kong' });
+      targetDate = new Date(nowHKT + 'T00:00:00+08:00');
+    }
+
+    const readings = await getObservedDailyReadings('hong_kong', targetDate);
 
     const data = readings.map((r) => ({
       temp: r.temp,

+ 75 - 13
packages/dashboard/src/app/cities/hong-kong/components/HKODailyCurve.tsx

@@ -1,6 +1,6 @@
 'use client';
 
-import { useState, useEffect, useCallback } from 'react';
+import { useState, useEffect, useCallback, useMemo } from 'react';
 import {
   AreaChart,
   Area,
@@ -18,13 +18,32 @@ interface Reading {
   fullTime: string; // for tooltip
 }
 
+function getTodayHKT(): string {
+  return new Date().toLocaleDateString('en-CA', { timeZone: 'Asia/Hong_Kong' });
+}
+
+function formatDateLabel(dateStr: string): string {
+  const [y, m, d] = dateStr.split('-');
+  return `${d}/${m}/${y}`;
+}
+
+function shiftDate(dateStr: string, days: number): string {
+  const d = new Date(dateStr + 'T00:00:00+08:00');
+  d.setDate(d.getDate() + days);
+  return d.toLocaleDateString('en-CA', { timeZone: 'Asia/Hong_Kong' });
+}
+
 export function HKODailyCurve() {
+  const [selectedDate, setSelectedDate] = useState(getTodayHKT);
   const [readings, setReadings] = useState<Reading[]>([]);
   const [loading, setLoading] = useState(true);
 
+  const isToday = useMemo(() => selectedDate === getTodayHKT(), [selectedDate]);
+
   const fetchReadings = useCallback(async () => {
     try {
-      const res = await fetch('/api/hko-daily-readings');
+      const url = `/api/hko-daily-readings?date=${selectedDate}`;
+      const res = await fetch(url);
       if (res.ok) {
         const data = await res.json();
         const parsed: Reading[] = (data.readings ?? []).map(
@@ -55,13 +74,52 @@ export function HKODailyCurve() {
     } finally {
       setLoading(false);
     }
-  }, []);
+  }, [selectedDate]);
 
   useEffect(() => {
+    setLoading(true);
     fetchReadings();
-    const interval = setInterval(fetchReadings, 3 * 60 * 1000); // refresh every 3 min
-    return () => clearInterval(interval);
-  }, [fetchReadings]);
+    if (isToday) {
+      const interval = setInterval(fetchReadings, 3 * 60 * 1000);
+      return () => clearInterval(interval);
+    }
+  }, [fetchReadings, isToday]);
+
+  const goBack = () => setSelectedDate((d) => shiftDate(d, -1));
+  const goForward = () => {
+    if (!isToday) setSelectedDate((d) => shiftDate(d, 1));
+  };
+  const goToday = () => setSelectedDate(getTodayHKT());
+
+  const title = isToday ? "Today's Temperature Curve" : `Temperature Curve — ${formatDateLabel(selectedDate)}`;
+
+  const dateNav = (
+    <div className="flex items-center gap-1.5">
+      <button
+        onClick={goBack}
+        className="p-1 rounded hover:bg-gray-100 dark:hover:bg-slate-700 text-dim"
+        title="Previous day"
+      >
+        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="15 18 9 12 15 6" /></svg>
+      </button>
+      {!isToday && (
+        <button
+          onClick={goToday}
+          className="px-1.5 py-0.5 rounded text-[10px] font-medium bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400 hover:bg-amber-200 dark:hover:bg-amber-900/50"
+        >
+          Today
+        </button>
+      )}
+      <button
+        onClick={goForward}
+        disabled={isToday}
+        className="p-1 rounded hover:bg-gray-100 dark:hover:bg-slate-700 text-dim disabled:opacity-30 disabled:cursor-not-allowed"
+        title="Next day"
+      >
+        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 18 15 12 9 6" /></svg>
+      </button>
+    </div>
+  );
 
   if (loading) {
     return (
@@ -75,11 +133,14 @@ export function HKODailyCurve() {
   if (readings.length < 2) {
     return (
       <div className="card p-6">
-        <h3 className="text-sm font-medium text-heading mb-2">
-          Today&apos;s Temperature Curve
-        </h3>
+        <div className="flex items-center justify-between mb-2">
+          <h3 className="text-sm font-medium text-heading">{title}</h3>
+          {dateNav}
+        </div>
         <p className="text-dim text-sm text-center py-8">
-          Not enough readings yet. Data accumulates throughout the day.
+          {isToday
+            ? 'Not enough readings yet. Data accumulates throughout the day.'
+            : 'No data available for this date.'}
         </p>
       </div>
     );
@@ -98,9 +159,10 @@ export function HKODailyCurve() {
     <div className="card p-6">
       <div className="flex items-start justify-between mb-4">
         <div>
-          <h3 className="text-sm font-medium text-heading">
-            Today&apos;s Temperature Curve
-          </h3>
+          <div className="flex items-center gap-2">
+            <h3 className="text-sm font-medium text-heading">{title}</h3>
+            {dateNav}
+          </div>
           <p className="text-xs text-dim mt-0.5">
             HKO Observatory &middot; {readings.length} readings
           </p>