nomadweb/frontend/src/components/JetLagEstimator.tsx
eric d1bc4af87f Add jet lag, housing guide, phrasebook, and feedback widget
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 23:28:52 -05:00

112 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { useMemo, useState } from "react";
import type { Destination } from "@/lib/types";
const TZ_OFFSET: Record<string, number> = {
bali: 8, lisbon: 0, chiangmai: 7, mexico: -6, barcelona: 1, tokyo: 9,
};
const HOME_OFFSETS = [
{ key: "cn", label: "🇨🇳 中国 (UTC+8)", offset: 8 },
{ key: "us_east", label: "🇺🇸 美东 (UTC-5)", offset: -5 },
{ key: "us_west", label: "🇺🇸 美西 (UTC-8)", offset: -8 },
{ key: "uk", label: "🇬🇧 英国 (UTC+0)", offset: 0 },
{ key: "eu", label: "🇪🇺 中欧 (UTC+1)", offset: 1 },
];
function tipsForDiff(absDiff: number): string[] {
if (absDiff <= 2) return ["时差很小,几乎无感", "抵达当天可正常作息", "保持常规睡眠即可"];
if (absDiff <= 5) return ["建议提前 2–3 天微调作息", "抵达后当天晒太阳帮助校准", "避免落地立刻开长会"];
if (absDiff <= 8) return ["东飞更伤,预留 2–3 天恢复", "第一晚尽量睡满 7 小时", "褪黑素/镁片可辅助(遵医嘱)"];
return ["跨半球长途,预留 4–5 天缓冲", "分段飞行比直达更易恢复", "前两天安排轻量工作即可"];
}
interface Props {
destinations: Destination[];
}
export default function JetLagEstimator({ destinations }: Props) {
const [home, setHome] = useState("cn");
const [slug, setSlug] = useState(destinations[0]?.slug || "bali");
const [direction, setDirection] = useState<"east" | "west">("east");
const result = useMemo(() => {
const homeOff = HOME_OFFSETS.find((h) => h.key === home)?.offset ?? 8;
const destOff = TZ_OFFSET[slug] ?? 8;
let diff = destOff - homeOff;
if (diff > 12) diff -= 24;
if (diff < -12) diff += 24;
const abs = Math.abs(diff);
const dir = diff >= 0 ? "east" : "west";
const recovery = Math.max(1, Math.ceil(abs / 2));
return { diff, abs, dir, recovery, tips: tipsForDiff(abs) };
}, [home, slug]);
const dest = destinations.find((d) => d.slug === slug);
return (
<section className="section jetlag-section" id="jetlag">
<div className="container">
<div className="section-header reveal">
<span className="section-tag">😴 JET LAG</span>
<h2>时差恢复估算</h2>
<p>算出时差跨度,提前安排恢复期与工作节奏</p>
</div>
<div className="jetlag-card reveal">
<div className="jetlag-form">
<div className="calc-field">
<label>🏠 当前时区</label>
<select value={home} onChange={(e) => setHome(e.target.value)}>
{HOME_OFFSETS.map((h) => (
<option key={h.key} value={h.key}>{h.label}</option>
))}
</select>
</div>
<div className="calc-field">
<label>✈️ 目的地</label>
<select value={slug} onChange={(e) => setSlug(e.target.value)}>
{destinations.map((d) => (
<option key={d.slug} value={d.slug}>{d.emoji} {d.name}</option>
))}
</select>
</div>
<div className="calc-field">
<label>🧭 飞行方向(主观)</label>
<div className="calc-options">
<button className={`calc-option${direction === "east" ? " active" : ""}`} onClick={() => setDirection("east")}>🌅 向东飞</button>
<button className={`calc-option${direction === "west" ? " active" : ""}`} onClick={() => setDirection("west")}>🌇 向西飞</button>
</div>
</div>
</div>
<div className="jetlag-result">
<div className="jetlag-diff">
<span className="jetlag-emoji">{dest?.emoji || "🌍"}</span>
<strong>{result.abs === 0 ? "无时差" : `${result.abs} 小时`}</strong>
<small>
{result.diff === 0
? "与当前时区相同"
: result.diff > 0
? `目的地快 ${result.abs} 小时`
: `目的地慢 ${result.abs} 小时`}
</small>
</div>
<div className="jetlag-recovery">
<span>建议恢复期</span>
<strong className="gradient-text">{result.recovery} 天</strong>
{(direction === "east" || result.dir === "east") && result.abs > 3 && (
<p className="jetlag-note">向东飞行通常更难适应,可多留一天缓冲</p>
)}
</div>
<ul className="jetlag-tips">
{result.tips.map((t) => <li key={t}>💡 {t}</li>)}
</ul>
</div>
</div>
</div>
</section>
);
}