nomadweb/frontend/src/components/ArrivalChecklist.tsx
eric bdf0516197 Fix dating match gate and expand seed data for usable demos.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-04 06:18:25 -05:00

134 lines
4.8 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";
import ToolCityExits from "@/components/ToolCityExits";
import { meetupCityHref } from "@/lib/meetupLinks";
import Link from "next/link";
interface Task {
id: string;
emoji: string;
label: string;
day: number;
}
const TASKS: Task[] = [
{ id: "sim", emoji: "📱", label: "办本地 SIM / 激活 eSIM", day: 1 },
{ id: "cash", emoji: "💵", label: "换少量现金 + 测试银行卡", day: 1 },
{ id: "wifi", emoji: "📶", label: "测网速,确认可视频会议", day: 1 },
{ id: "grocery", emoji: "🛒", label: "采购日用品与饮水", day: 1 },
{ id: "maps", emoji: "🗺️", label: "标记医院 / 警局 / 超市", day: 2 },
{ id: "cowork", emoji: "💻", label: "探访 1–2 个联合办公", day: 2 },
{ id: "cafe", emoji: "☕", label: "找到常去的工作咖啡馆", day: 3 },
{ id: "meetup", emoji: "🤝", label: "加本地游民群 / 报名 Meetup", day: 3 },
{ id: "housing", emoji: "🏡", label: "确认长租或续住决策", day: 5 },
{ id: "routine", emoji: "🕐", label: "固定作息与深度工作时段", day: 7 },
{ id: "backup", emoji: "🔐", label: "备份证件照片到云盘", day: 2 },
{ id: "insurance", emoji: "🏥", label: "确认保险客服与紧急电话", day: 1 },
];
const STORAGE_PREFIX = "nomadro-arrival-";
interface Props {
destinations: Destination[];
}
export default function ArrivalChecklist({ destinations }: Props) {
const [slug, setSlug] = useState(destinations[0]?.slug || "bali");
const [checked, setChecked] = useState<Record<string, boolean>>({});
const [ready, setReady] = useState(false);
const dest = destinations.find((d) => d.slug === slug);
useEffect(() => {
try {
const raw = localStorage.getItem(STORAGE_PREFIX + slug);
setChecked(raw ? JSON.parse(raw) : {});
} catch {
setChecked({});
}
setReady(true);
}, [slug]);
useEffect(() => {
if (!ready) return;
localStorage.setItem(STORAGE_PREFIX + slug, JSON.stringify(checked));
}, [checked, slug, ready]);
const done = Object.values(checked).filter(Boolean).length;
const pct = Math.round((done / TASKS.length) * 100);
const byDay = useMemo(() => {
const map: Record<number, Task[]> = {};
TASKS.forEach((t) => {
if (!map[t.day]) map[t.day] = [];
map[t.day].push(t);
});
return Object.entries(map).sort((a, b) => Number(a[0]) - Number(b[0]));
}, []);
const toggle = (id: string) => setChecked((p) => ({ ...p, [id]: !p[id] }));
return (
<section className="section arrival-section" id="arrival">
<div className="container">
<div className="section-header reveal">
<span className="section-tag">🛬 ARRIVAL</span>
<h2>落地第一周清单</h2>
<p>从落地到进入工作节奏,按天勾选不慌乱</p>
</div>
<div className="housing-cities reveal">
{destinations.map((d) => (
<button
key={d.slug}
className={`season-city-btn${slug === d.slug ? " active" : ""}`}
onClick={() => setSlug(d.slug)}
>
{d.emoji} {d.name}
</button>
))}
</div>
<div className="arrival-card reveal">
<div className="arrival-progress">
<div>
<h3>{dest?.emoji} {dest?.name} · 到站进度</h3>
<p>{done}/{TASKS.length} 完成 · {pct}%</p>
</div>
<div className="packing-bar" style={{ maxWidth: 240, flex: 1 }}>
<div className="packing-bar-fill" style={{ width: `${pct}%` }} />
</div>
</div>
<div className="arrival-days">
{byDay.map(([day, tasks]) => (
<div key={day} className="arrival-day">
<h4>Day {day}</h4>
<ul>
{tasks.map((t) => (
<li key={t.id}>
<label className={`packing-item${checked[t.id] ? " checked" : ""}`}>
<input type="checkbox" checked={!!checked[t.id]} onChange={() => toggle(t.id)} />
<span className="packing-check">{checked[t.id] ? "✓" : ""}</span>
<span className="packing-emoji">{t.emoji}</span>
<span className="packing-label">{t.label}</span>
{t.id === "meetup" && dest && (
<Link href={meetupCityHref(dest.name)} className="arrival-meetup-link">
→
</Link>
)}
</label>
</li>
))}
</ul>
</div>
))}
</div>
{dest && <ToolCityExits dest={dest} />}
</div>
</div>
</section>
);
}