|
@@ -1,6 +1,6 @@
|
|
|
'use client';
|
|
'use client';
|
|
|
|
|
|
|
|
-import { useState, useEffect, useCallback } from 'react';
|
|
|
|
|
|
|
+import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
|
import {
|
|
import {
|
|
|
AreaChart,
|
|
AreaChart,
|
|
|
Area,
|
|
Area,
|
|
@@ -18,13 +18,32 @@ interface Reading {
|
|
|
fullTime: string; // for tooltip
|
|
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() {
|
|
export function HKODailyCurve() {
|
|
|
|
|
+ const [selectedDate, setSelectedDate] = useState(getTodayHKT);
|
|
|
const [readings, setReadings] = useState<Reading[]>([]);
|
|
const [readings, setReadings] = useState<Reading[]>([]);
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
|
|
|
|
+ const isToday = useMemo(() => selectedDate === getTodayHKT(), [selectedDate]);
|
|
|
|
|
+
|
|
|
const fetchReadings = useCallback(async () => {
|
|
const fetchReadings = useCallback(async () => {
|
|
|
try {
|
|
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) {
|
|
if (res.ok) {
|
|
|
const data = await res.json();
|
|
const data = await res.json();
|
|
|
const parsed: Reading[] = (data.readings ?? []).map(
|
|
const parsed: Reading[] = (data.readings ?? []).map(
|
|
@@ -55,13 +74,52 @@ export function HKODailyCurve() {
|
|
|
} finally {
|
|
} finally {
|
|
|
setLoading(false);
|
|
setLoading(false);
|
|
|
}
|
|
}
|
|
|
- }, []);
|
|
|
|
|
|
|
+ }, [selectedDate]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
|
|
+ setLoading(true);
|
|
|
fetchReadings();
|
|
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) {
|
|
if (loading) {
|
|
|
return (
|
|
return (
|
|
@@ -75,11 +133,14 @@ export function HKODailyCurve() {
|
|
|
if (readings.length < 2) {
|
|
if (readings.length < 2) {
|
|
|
return (
|
|
return (
|
|
|
<div className="card p-6">
|
|
<div className="card p-6">
|
|
|
- <h3 className="text-sm font-medium text-heading mb-2">
|
|
|
|
|
- Today'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">
|
|
<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>
|
|
</p>
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|
|
@@ -98,9 +159,10 @@ export function HKODailyCurve() {
|
|
|
<div className="card p-6">
|
|
<div className="card p-6">
|
|
|
<div className="flex items-start justify-between mb-4">
|
|
<div className="flex items-start justify-between mb-4">
|
|
|
<div>
|
|
<div>
|
|
|
- <h3 className="text-sm font-medium text-heading">
|
|
|
|
|
- Today'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">
|
|
<p className="text-xs text-dim mt-0.5">
|
|
|
HKO Observatory · {readings.length} readings
|
|
HKO Observatory · {readings.length} readings
|
|
|
</p>
|
|
</p>
|