nomadweb/frontend/src/components/TimezoneBoard.tsx
2026-08-28 19:49:10 -05:00

177 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useEffect, useMemo, useState } from "react";
import type { Destination } from "@/lib/types";
const TZ_OFFSETS: Record<string, number> = {
bali: 8,
lisbon: 0,
chiangmai: 7,
mexico: -6,
barcelona: 1,
tokyo: 9,
};
const HOME_TZ = 8; // China UTC+8
const WORK_START = 9;
const WORK_END = 18;
interface Props {
destinations: Destination[];
}
function getLocalHour(utcOffset: number): number {
const now = new Date();
const utc = now.getUTCHours() + now.getUTCMinutes() / 60;
return Math.floor((utc + utcOffset + 24) % 24);
}
function formatTime(hour: number, min: number): string {
return `${String(hour).padStart(2, "0")}:${String(min).padStart(2, "0")}`;
}
function getOverlapHours(destOffset: number, homeStart: number, homeEnd: number): number {
let overlap = 0;
for (let h = homeStart; h < homeEnd; h++) {
const destHour = (h - HOME_TZ + destOffset + 24) % 24;
if (destHour >= 8 && destHour < 20) overlap++;
}
return overlap;
}
function overlapLabel(hours: number): { text: string; class: string } {
if (hours >= 7) return { text: "极佳", class: "excellent" };
if (hours >= 5) return { text: "良好", class: "good" };
if (hours >= 3) return { text: "一般", class: "fair" };
return { text: "困难", class: "poor" };
}
export default function TimezoneBoard({ destinations }: Props) {
const [now, setNow] = useState(new Date());
const [homeStart, setHomeStart] = useState(WORK_START);
const [homeEnd, setHomeEnd] = useState(WORK_END);
useEffect(() => {
const timer = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(timer);
}, []);
const cities = useMemo(() =>
destinations
.filter((d) => TZ_OFFSETS[d.slug] !== undefined)
.map((d) => {
const offset = TZ_OFFSETS[d.slug];
const localHour = getLocalHour(offset);
const overlap = getOverlapHours(offset, homeStart, homeEnd);
const label = overlapLabel(overlap);
const isWorkTime = localHour >= 9 && localHour < 18;
return { ...d, offset, localHour, overlap, label, isWorkTime };
})
.sort((a, b) => b.overlap - a.overlap),
[destinations, homeStart, homeEnd]);
const homeTime = formatTime(now.getHours(), now.getMinutes());
const homeSeconds = now.getSeconds();
return (
<section className="section timezone-section" id="timezone">
<div className="container">
<div className="section-header reveal">
<span className="section-tag">🕐 TIMEZONE</span>
<h2>时区工作看板</h2>
<p>实时查看各城市当地时间,评估与国内团队的工作时间重叠</p>
</div>
<div className="tz-home-clock reveal">
<div className="tz-home-time">
<span className="tz-live-dot" />
<span className="tz-home-label">🇨🇳 北京时间</span>
<span className="tz-home-digits">
{homeTime}<span className="tz-seconds">:{String(homeSeconds).padStart(2, "0")}</span>
</span>
</div>
<div className="tz-work-slider">
<label>⏰ 你的工作时间: {homeStart}:00 – {homeEnd}:00</label>
<div className="tz-slider-row">
<span>开始</span>
<input type="range" min={6} max={12} value={homeStart}
onChange={(e) => setHomeStart(Math.min(+e.target.value, homeEnd - 1))} />
<span>{homeStart}:00</span>
</div>
<div className="tz-slider-row">
<span>结束</span>
<input type="range" min={13} max={22} value={homeEnd}
onChange={(e) => setHomeEnd(Math.max(+e.target.value, homeStart + 1))} />
<span>{homeEnd}:00</span>
</div>
</div>
</div>
<div className="tz-grid reveal">
{cities.map((city) => {
const localMin = now.getUTCMinutes();
const localTime = formatTime(city.localHour, localMin);
const angle = ((city.localHour % 12) + localMin / 60) * 30;
return (
<div key={city.slug} className={`tz-card${city.isWorkTime ? " working" : ""}`}>
<div className="tz-card-header">
<span className="tz-card-emoji">{city.emoji}</span>
<div>
<strong>{city.name}</strong>
<span>UTC{city.offset >= 0 ? "+" : ""}{city.offset}</span>
</div>
<span className={`tz-overlap-badge ${city.label.class}`}>
{city.label.text}
</span>
</div>
<div className="tz-analog">
<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="44" fill="none" stroke="rgba(255,255,255,0.06)" strokeWidth="2" />
{[0, 3, 6, 9].map((n) => (
<line key={n}
x1="50" y1="10" x2="50" y2="16"
stroke="rgba(255,255,255,0.15)" strokeWidth="1.5"
transform={`rotate(${n * 30} 50 50)`}
/>
))}
<line x1="50" y1="50" x2="50" y2="22"
stroke="url(#tzHand)" strokeWidth="2.5" strokeLinecap="round"
transform={`rotate(${angle} 50 50)`}
/>
<circle cx="50" cy="50" r="3" fill="#4ECDC4" />
</svg>
<span className="tz-digital">{localTime}</span>
</div>
<div className="tz-overlap-bar">
<div className="tz-overlap-track">
<div
className={`tz-overlap-fill ${city.label.class}`}
style={{ width: `${(city.overlap / Math.max(homeEnd - homeStart, 1)) * 100}%` }}
/>
</div>
<span>重叠 {city.overlap}h / {homeEnd - homeStart}h</span>
</div>
<div className="tz-status">
{city.isWorkTime ? "💼 当地工作时间" : "🌙 当地非工作时间"}
</div>
</div>
);
})}
</div>
<svg width="0" height="0">
<defs>
<linearGradient id="tzHand" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#FF6B6B" />
<stop offset="100%" stopColor="#4ECDC4" />
</linearGradient>
</defs>
</svg>
</div>
</section>
);
}