134 lines
4.8 KiB
TypeScript
134 lines
4.8 KiB
TypeScript
"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>
|
||
);
|
||
}
|