Add tax day counter, arrival checklist, cafe guide, and onboarding

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
eric 2026-08-29 00:13:19 -05:00
parent 341fcdd15e
commit 916ce30733
9 changed files with 711 additions and 3 deletions

View File

@ -119,6 +119,10 @@ docker compose up -d
| 📒 支出账本 | 旅居花销分类记录与汇总 | | 📒 支出账本 | 旅居花销分类记录与汇总 |
| 📱 上网方案 | 各城 SIM / eSIM 推荐 | | 📱 上网方案 | 各城 SIM / eSIM 推荐 |
| 🌏 关于页 | `/about` 品牌介绍 | | 🌏 关于页 | `/about` 品牌介绍 |
| 🧾 税居天数 | 183 天规则停留天数追踪 |
| 🛬 到站清单 | 落地第一周按天勾选 |
| ☕ 咖啡馆 | 办公友好咖啡馆推荐 |
| 👋 新手引导 | 首次访问四步导览 |
| 📝 博客 | 标签筛选 + 阅读进度 + 相关推荐 + 分享 | | 📝 博客 | 标签筛选 + 阅读进度 + 相关推荐 + 分享 |
| ❓ FAQ | 搜索 + 展开/收起全部 | | ❓ FAQ | 搜索 + 展开/收起全部 |

View File

@ -6822,3 +6822,262 @@ img { max-width: 100%; display: block; }
.expense-card { grid-template-columns: 1fr; } .expense-card { grid-template-columns: 1fr; }
.expense-bar-row { grid-template-columns: 60px 1fr 60px; } .expense-bar-row { grid-template-columns: 60px 1fr 60px; }
} }
/* ===== Tax Day Counter ===== */
.tax-card {
padding: 28px;
background: var(--bg-card);
border: var(--border-glass);
border-radius: var(--radius-xl);
backdrop-filter: blur(20px);
}
.tax-meter {
display: flex;
gap: 28px;
align-items: center;
margin-bottom: 24px;
flex-wrap: wrap;
}
.tax-ring {
width: 140px;
height: 140px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.tax-risk-warn { background: conic-gradient(#FFE66D var(--pct, 50%), rgba(255,255,255,0.08) 0) !important; }
.tax-risk-high { background: conic-gradient(#FF6B6B var(--pct, 50%), rgba(255,255,255,0.08) 0) !important; }
.tax-ring-inner {
width: 110px;
height: 110px;
border-radius: 50%;
background: var(--bg-card);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.tax-ring-inner strong { font-size: 1.8rem; }
.tax-ring-inner small { color: var(--text-secondary); font-size: 0.75rem; }
.tax-stats {
flex: 1;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
min-width: 240px;
}
.tax-stats > div small {
display: block;
font-size: 0.75rem;
color: var(--text-secondary);
margin-bottom: 2px;
}
.tax-stats > div strong { font-size: 1.2rem; }
.tax-danger { color: #FF6B6B; }
.tax-hint {
grid-column: 1 / -1;
font-size: 0.85rem;
color: var(--text-secondary);
margin: 4px 0 0;
}
.tax-form {
display: flex;
flex-direction: column;
gap: 14px;
margin-bottom: 20px;
padding-top: 16px;
border-top: 1px solid rgba(255,255,255,0.06);
}
.tax-log {
list-style: none;
padding: 0;
margin: 0;
max-height: 220px;
overflow-y: auto;
}
.tax-log li {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
border-bottom: 1px solid rgba(255,255,255,0.05);
font-size: 0.9rem;
}
.tax-log li button {
margin-left: auto;
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
}
.tax-empty {
justify-content: center;
color: var(--text-secondary);
border: none !important;
}
/* ===== Arrival Checklist ===== */
.arrival-card {
padding: 28px;
background: var(--bg-card);
border: var(--border-glass);
border-radius: var(--radius-xl);
backdrop-filter: blur(20px);
}
.arrival-progress {
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.arrival-progress h3 { font-size: 1.15rem; margin-bottom: 4px; }
.arrival-progress p { color: var(--text-secondary); font-size: 0.9rem; }
.arrival-days {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 18px;
}
.arrival-day h4 {
font-size: 0.95rem;
color: var(--accent-3);
margin-bottom: 10px;
}
.arrival-day ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
/* ===== Cafe Guide ===== */
.cafe-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 16px;
}
.cafe-card {
padding: 20px;
background: var(--bg-card);
border: var(--border-glass);
border-radius: var(--radius-lg);
backdrop-filter: blur(16px);
transition: transform 0.3s;
}
.cafe-card:hover { transform: translateY(-3px); }
.cafe-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 10px;
margin-bottom: 10px;
}
.cafe-top h4 { font-size: 1.05rem; }
.cafe-vibe {
font-size: 0.75rem;
padding: 4px 8px;
background: rgba(255,230,109,0.12);
color: var(--accent-2);
border-radius: 12px;
white-space: nowrap;
}
.cafe-tags {
display: flex;
gap: 10px;
flex-wrap: wrap;
font-size: 0.8rem;
color: var(--accent-3);
margin-bottom: 8px;
}
.cafe-card p {
font-size: 0.85rem;
color: var(--text-secondary);
line-height: 1.45;
}
/* ===== Onboarding ===== */
.onboard-card {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 9200;
width: min(380px, calc(100% - 32px));
padding: 20px;
background: var(--bg-card);
border: var(--border-glass);
border-radius: var(--radius-xl);
backdrop-filter: blur(20px);
box-shadow: var(--shadow-card);
animation: toastIn 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
text-align: center;
}
.onboard-skip {
position: absolute;
top: 10px;
right: 12px;
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
font-size: 0.8rem;
}
.onboard-emoji { font-size: 2rem; display: block; margin-bottom: 8px; }
.onboard-step strong { display: block; font-size: 1.15rem; margin-bottom: 6px; }
.onboard-step p { color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 14px; }
.onboard-dots {
display: flex;
justify-content: center;
gap: 6px;
margin-bottom: 14px;
}
.onboard-dots span {
width: 6px;
height: 6px;
border-radius: 50%;
background: rgba(255,255,255,0.2);
}
.onboard-dots span.active {
width: 18px;
border-radius: 4px;
background: var(--accent-3);
}
@media (max-width: 768px) {
.tax-stats { grid-template-columns: 1fr 1fr 1fr; }
.onboard-card { bottom: 90px; }
}

View File

@ -0,0 +1,124 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import type { Destination } from "@/lib/types";
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>
</label>
</li>
))}
</ul>
</div>
))}
</div>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,97 @@
"use client";
import { useMemo, useState } from "react";
import type { Destination } from "@/lib/types";
interface Cafe {
name: string;
vibe: string;
wifi: string;
power: string;
tip: string;
}
const CAFES: Record<string, Cafe[]> = {
bali: [
{ name: "Seniman Coffee", vibe: "乌布创意", wifi: "快", power: "充足", tip: "适合上午深度工作" },
{ name: "Revolver Espresso", vibe: "精致澳式", wifi: "稳", power: "有限", tip: "偏短会与头脑风暴" },
{ name: "Anomali Coffee", vibe: "连锁便利", wifi: "中", power: "充足", tip: "多点可选,备选办公" },
],
chiangmai: [
{ name: "Ristr8to", vibe: "拉花圣地", wifi: "快", power: "中等", tip: "拍照友好,下午较满" },
{ name: "Graph Cafe", vibe: "极简静谧", wifi: "很快", power: "充足", tip: "写代码首选" },
{ name: "The Lab", vibe: "实验风", wifi: "稳", power: "充足", tip: "长坐友好" },
],
lisbon: [
{ name: "Copenhagen Coffee Lab", vibe: "北欧风", wifi: "快", power: "中等", tip: "适合上午冲刺" },
{ name: "Fábrica Coffee Roasters", vibe: "烘焙香", wifi: "稳", power: "有限", tip: "短时办公 + 咖啡" },
{ name: "Hello, Kristof", vibe: "设计感", wifi: "快", power: "充足", tip: "会议间隙好去处" },
],
barcelona: [
{ name: "Satan's Coffee Corner", vibe: "潮酷", wifi: "稳", power: "中等", tip: "人气高,早点去" },
{ name: "Nomad Coffee", vibe: "专注", wifi: "快", power: "充足", tip: "名副其实的游民店" },
{ name: "Syra Coffee", vibe: "社区感", wifi: "快", power: "充足", tip: "多门店可轮换" },
],
mexico: [
{ name: "Quince Letras", vibe: "文艺", wifi: "稳", power: "中等", tip: "Roma 区散步办公" },
{ name: "Blend Station", vibe: "明亮", wifi: "快", power: "充足", tip: "长坐友好" },
{ name: "Cardinal", vibe: "精致", wifi: "快", power: "有限", tip: "适合客户会面前" },
],
tokyo: [
{ name: " % Arabica", vibe: "极简", wifi: "中", power: "少", tip: "短时灵感,不适合长坐" },
{ name: "Blue Bottle", vibe: "安静", wifi: "快", power: "中等", tip: "上午工作节奏佳" },
{ name: "Fuglen Tokyo", vibe: "北欧夜色", wifi: "稳", power: "中等", tip: "傍晚切换心情" },
],
};
interface Props {
destinations: Destination[];
}
export default function CafeGuide({ destinations }: Props) {
const [slug, setSlug] = useState(destinations[0]?.slug || "chiangmai");
const list = useMemo(() => CAFES[slug] || CAFES.chiangmai, [slug]);
const dest = destinations.find((d) => d.slug === slug);
return (
<section className="section cafe-section" id="cafes">
<div className="container">
<div className="section-header reveal">
<span className="section-tag">☕ CAFES</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>
<h3 className="sim-city-title reveal">{dest?.emoji} {dest?.name} 精选</h3>
<div className="cafe-grid">
{list.map((c, i) => (
<div key={c.name} className="cafe-card reveal" style={{ transitionDelay: `${i * 0.08}s` }}>
<div className="cafe-top">
<h4>☕ {c.name}</h4>
<span className="cafe-vibe">{c.vibe}</span>
</div>
<div className="cafe-tags">
<span>📶 WiFi {c.wifi}</span>
<span>🔌 插座 {c.power}</span>
</div>
<p>{c.tip}</p>
</div>
))}
</div>
</div>
</section>
);
}

View File

@ -42,17 +42,17 @@ export default function Footer() {
<Link href="/#destinations">目的地</Link> <Link href="/#destinations">目的地</Link>
<Link href="/#spin">命运转盘</Link> <Link href="/#spin">命运转盘</Link>
<Link href="/#wifi">网速评级</Link> <Link href="/#wifi">网速评级</Link>
<Link href="/#cafes">咖啡馆</Link>
<Link href="/#arrival">到站清单</Link>
<Link href="/#insurance">保险指南</Link> <Link href="/#insurance">保险指南</Link>
<Link href="/#sim">上网方案</Link> <Link href="/#sim">上网方案</Link>
<Link href="/#housing">住宿指南</Link>
<Link href="/#phrases">生存短语</Link>
<Link href="/#blog">博客</Link> <Link href="/#blog">博客</Link>
</div> </div>
<div className="footer-links"> <div className="footer-links">
<h4>工具</h4> <h4>工具</h4>
<Link href="/#trip">行程规划</Link> <Link href="/#trip">行程规划</Link>
<Link href="/#expenses">支出账本</Link> <Link href="/#expenses">支出账本</Link>
<Link href="/#jetlag">时差估算</Link> <Link href="/#tax-days">税居天数</Link>
<Link href="/#savings">启动金目标</Link> <Link href="/#savings">启动金目标</Link>
<Link href="/#calculator">费用计算器</Link> <Link href="/#calculator">费用计算器</Link>
<a href={`${SITE}/docs`} target="_blank" rel="noopener noreferrer">API 文档</a> <a href={`${SITE}/docs`} target="_blank" rel="noopener noreferrer">API 文档</a>

View File

@ -47,6 +47,10 @@ import FeedbackWidget from "./FeedbackWidget";
import InsuranceGuide from "./InsuranceGuide"; import InsuranceGuide from "./InsuranceGuide";
import ExpenseTracker from "./ExpenseTracker"; import ExpenseTracker from "./ExpenseTracker";
import SimGuide from "./SimGuide"; import SimGuide from "./SimGuide";
import TaxDayCounter from "./TaxDayCounter";
import ArrivalChecklist from "./ArrivalChecklist";
import CafeGuide from "./CafeGuide";
import OnboardingTour from "./OnboardingTour";
import KeyboardShortcuts from "./KeyboardShortcuts"; import KeyboardShortcuts from "./KeyboardShortcuts";
import BackToTop from "./BackToTop"; import BackToTop from "./BackToTop";
@ -125,6 +129,7 @@ export default function HomeClient(props: Props) {
<CostCompare destinations={props.destinations} /> <CostCompare destinations={props.destinations} />
<SavingsGoal destinations={props.destinations} /> <SavingsGoal destinations={props.destinations} />
<ExpenseTracker /> <ExpenseTracker />
<TaxDayCounter />
<FlightCost destinations={props.destinations} /> <FlightCost destinations={props.destinations} />
<CurrencyConverter /> <CurrencyConverter />
<WifiWorkScore destinations={props.destinations} /> <WifiWorkScore destinations={props.destinations} />
@ -136,8 +141,10 @@ export default function HomeClient(props: Props) {
<CoworkingGuide /> <CoworkingGuide />
<HousingGuide destinations={props.destinations} /> <HousingGuide destinations={props.destinations} />
<SimGuide destinations={props.destinations} /> <SimGuide destinations={props.destinations} />
<CafeGuide destinations={props.destinations} />
<SeasonGuide destinations={props.destinations} /> <SeasonGuide destinations={props.destinations} />
<PackingChecklist /> <PackingChecklist />
<ArrivalChecklist destinations={props.destinations} />
<Phrasebook destinations={props.destinations} /> <Phrasebook destinations={props.destinations} />
<NomadEvents /> <NomadEvents />
<Tools tools={props.tools} /> <Tools tools={props.tools} />
@ -153,6 +160,7 @@ export default function HomeClient(props: Props) {
<CookieConsent /> <CookieConsent />
<NomadTips /> <NomadTips />
<FeedbackWidget /> <FeedbackWidget />
<OnboardingTour />
{compareList.length > 0 && ( {compareList.length > 0 && (
<div className="compare-bar"> <div className="compare-bar">

View File

@ -0,0 +1,63 @@
"use client";
import { useEffect, useState } from "react";
const STEPS = [
{ id: "destinations", emoji: "🌍", title: "探索目的地", desc: "筛选城市、收藏与对比" },
{ id: "matcher", emoji: "🎯", title: "智能匹配", desc: "问卷推荐最适合你的城市" },
{ id: "trip", emoji: "🗓️", title: "规划行程", desc: "多城串联并估算预算" },
{ id: "tools", emoji: "🛠️", title: "用好工具", desc: "签证、保险、网速与账本" },
];
export default function OnboardingTour() {
const [open, setOpen] = useState(false);
const [step, setStep] = useState(0);
useEffect(() => {
if (localStorage.getItem("nomadro-onboarded")) return;
const t = setTimeout(() => setOpen(true), 2500);
return () => clearTimeout(t);
}, []);
const finish = () => {
localStorage.setItem("nomadro-onboarded", "1");
setOpen(false);
};
const next = () => {
if (step >= STEPS.length - 1) {
finish();
return;
}
const s = STEPS[step];
if (s.id === "matcher") {
window.dispatchEvent(new CustomEvent("open-matcher"));
} else {
document.getElementById(s.id)?.scrollIntoView({ behavior: "smooth" });
}
setStep((n) => n + 1);
};
if (!open) return null;
const current = STEPS[step];
return (
<div className="onboard-card" role="dialog" aria-label="新手引导">
<button className="onboard-skip" onClick={finish}>跳过</button>
<div className="onboard-step">
<span className="onboard-emoji">{current.emoji}</span>
<strong>{current.title}</strong>
<p>{current.desc}</p>
<div className="onboard-dots">
{STEPS.map((_, i) => (
<span key={i} className={i === step ? "active" : ""} />
))}
</div>
<button className="btn btn-primary btn-sm" onClick={next}>
{step >= STEPS.length - 1 ? "开始探索 🚀" : "下一步 →"}
</button>
</div>
</div>
);
}

View File

@ -13,6 +13,7 @@ const SECTIONS = [
{ id: "cost-compare", emoji: "💰", label: "省钱" }, { id: "cost-compare", emoji: "💰", label: "省钱" },
{ id: "savings", emoji: "🎯", label: "存款" }, { id: "savings", emoji: "🎯", label: "存款" },
{ id: "expenses", emoji: "📒", label: "账本" }, { id: "expenses", emoji: "📒", label: "账本" },
{ id: "tax-days", emoji: "🧾", label: "税居" },
{ id: "flight", emoji: "✈️", label: "机票" }, { id: "flight", emoji: "✈️", label: "机票" },
{ id: "wifi", emoji: "📶", label: "网速" }, { id: "wifi", emoji: "📶", label: "网速" },
{ id: "lifestyle", emoji: "💻", label: "生活" }, { id: "lifestyle", emoji: "💻", label: "生活" },
@ -21,8 +22,10 @@ const SECTIONS = [
{ id: "coworking", emoji: "💻", label: "办公" }, { id: "coworking", emoji: "💻", label: "办公" },
{ id: "housing", emoji: "🏡", label: "住宿" }, { id: "housing", emoji: "🏡", label: "住宿" },
{ id: "sim", emoji: "📱", label: "上网" }, { id: "sim", emoji: "📱", label: "上网" },
{ id: "cafes", emoji: "☕", label: "咖啡" },
{ id: "season", emoji: "🌤️", label: "季节" }, { id: "season", emoji: "🌤️", label: "季节" },
{ id: "packing", emoji: "🧳", label: "行李" }, { id: "packing", emoji: "🧳", label: "行李" },
{ id: "arrival", emoji: "🛬", label: "到站" },
{ id: "phrases", emoji: "🗣️", label: "短语" }, { id: "phrases", emoji: "🗣️", label: "短语" },
{ id: "events", emoji: "📅", label: "活动" }, { id: "events", emoji: "📅", label: "活动" },
{ id: "blog", emoji: "📝", label: "博客" }, { id: "blog", emoji: "📝", label: "博客" },

View File

@ -0,0 +1,150 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { useToast } from "@/lib/toast";
const STORAGE_KEY = "nomadro-tax-days";
interface DayLog {
date: string;
country: string;
emoji: string;
}
const COUNTRIES = [
{ key: "CN", emoji: "🇨🇳", label: "中国" },
{ key: "TH", emoji: "🇹🇭", label: "泰国" },
{ key: "ID", emoji: "🇮🇩", label: "印尼" },
{ key: "PT", emoji: "🇵🇹", label: "葡萄牙" },
{ key: "ES", emoji: "🇪🇸", label: "西班牙" },
{ key: "MX", emoji: "🇲🇽", label: "墨西哥" },
{ key: "JP", emoji: "🇯🇵", label: "日本" },
{ key: "OTHER", emoji: "🌍", label: "其他" },
];
export default function TaxDayCounter() {
const { toast } = useToast();
const [logs, setLogs] = useState<DayLog[]>([]);
const [ready, setReady] = useState(false);
const [country, setCountry] = useState("TH");
const [date, setDate] = useState(() => new Date().toISOString().slice(0, 10));
useEffect(() => {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) setLogs(JSON.parse(raw));
} catch { /* ignore */ }
setReady(true);
}, []);
useEffect(() => {
if (!ready) return;
localStorage.setItem(STORAGE_KEY, JSON.stringify(logs));
}, [logs, ready]);
const year = new Date().getFullYear();
const yearLogs = useMemo(
() => logs.filter((l) => l.date.startsWith(String(year))),
[logs, year]
);
const abroadDays = useMemo(
() => yearLogs.filter((l) => l.country !== "CN").length,
[yearLogs]
);
const homeDays = useMemo(
() => yearLogs.filter((l) => l.country === "CN").length,
[yearLogs]
);
const threshold = 183;
const remaining = Math.max(threshold - abroadDays, 0);
const pct = Math.min((abroadDays / threshold) * 100, 100);
const risk = abroadDays >= threshold ? "high" : abroadDays >= threshold - 30 ? "warn" : "ok";
const ringColor = risk === "high" ? "#FF6B6B" : risk === "warn" ? "#FFE66D" : "var(--accent-3)";
const addDay = () => {
if (logs.some((l) => l.date === date)) {
toast("该日期已记录", "info");
return;
}
const c = COUNTRIES.find((x) => x.key === country)!;
setLogs((prev) => [...prev, { date, country: country, emoji: c.emoji }].sort((a, b) => b.date.localeCompare(a.date)));
toast(`已记录 ${date} · ${c.emoji} ${c.label}`);
};
const remove = (d: string) => setLogs((prev) => prev.filter((l) => l.date !== d));
return (
<section className="section tax-section" id="tax-days">
<div className="container">
<div className="section-header reveal">
<span className="section-tag">🧾 TAX DAYS</span>
<h2>税务居民天数计数</h2>
<p>追踪 {year} 年境外停留天数,靠近 183 天规则时提前规划</p>
</div>
<div className="tax-card reveal">
<div className="tax-meter">
<div className="tax-ring" style={{ background: `conic-gradient(${ringColor} ${pct}%, rgba(255,255,255,0.08) 0)` }}>
<div className="tax-ring-inner">
<strong>{abroadDays}</strong>
<small>/ {threshold} 天</small>
</div>
</div>
<div className="tax-stats">
<div><small>境外天数</small><strong>{abroadDays}</strong></div>
<div><small>境内天数</small><strong>{homeDays}</strong></div>
<div>
<small>距 183 天</small>
<strong className={risk === "high" ? "tax-danger" : ""}>
{abroadDays >= threshold ? "已越过" : `还差 ${remaining}`}
</strong>
</div>
<p className="tax-hint">
{risk === "high" && "⚠️ 已达常见税务居民门槛,建议咨询专业顾问"}
{risk === "warn" && "⏰ 接近 183 天,留意停留与签证策略"}
{risk === "ok" && "✅ 目前相对安全,继续记录每一天"}
</p>
</div>
</div>
<div className="tax-form">
<div className="calc-field">
<label>📅 日期</label>
<input type="date" value={date} onChange={(e) => setDate(e.target.value)} />
</div>
<div className="calc-field">
<label>📍 所在国家</label>
<div className="expense-cats">
{COUNTRIES.map((c) => (
<button
key={c.key}
type="button"
className={`filter-btn${country === c.key ? " active" : ""}`}
onClick={() => setCountry(c.key)}
>
{c.emoji} {c.label}
</button>
))}
</div>
</div>
<button className="btn btn-primary" type="button" onClick={addDay}>➕ 记录当天</button>
</div>
<ul className="tax-log">
{yearLogs.slice(0, 10).map((l) => (
<li key={l.date}>
<span>{l.emoji}</span>
<strong>{l.date}</strong>
<span>{COUNTRIES.find((c) => c.key === l.country)?.label}</span>
<button type="button" onClick={() => remove(l.date)} aria-label="删除">✕</button>
</li>
))}
{yearLogs.length === 0 && <li className="tax-empty">还没有记录,从今天开始打卡</li>}
</ul>
</div>
</div>
</section>
);
}