147 lines
6.0 KiB
TypeScript
147 lines
6.0 KiB
TypeScript
"use client";
|
||
|
||
import { useMemo, useState } from "react";
|
||
import Link from "next/link";
|
||
import type { Destination } from "@/lib/types";
|
||
import { useI18n } from "@/lib/i18n";
|
||
import { useToast } from "@/lib/toast";
|
||
import { mergeDestinationsIntoTrip } from "@/lib/tripActions";
|
||
import { meetupCityHref } from "@/lib/meetupLinks";
|
||
|
||
/** First-month one-time + rent estimates in CNY. */
|
||
const LANDING: Record<string, { rent: number; depositMult: number; sim: number; transfer: number; misc: number }> = {
|
||
chiangmai: { rent: 2800, depositMult: 1, sim: 80, transfer: 120, misc: 400 },
|
||
bali: { rent: 3500, depositMult: 1, sim: 100, transfer: 200, misc: 500 },
|
||
lisbon: { rent: 6500, depositMult: 2, sim: 150, transfer: 180, misc: 800 },
|
||
barcelona: { rent: 7000, depositMult: 2, sim: 150, transfer: 200, misc: 900 },
|
||
mexico: { rent: 4500, depositMult: 1, sim: 120, transfer: 250, misc: 600 },
|
||
tokyo: { rent: 9000, depositMult: 2, sim: 200, transfer: 300, misc: 1200 },
|
||
};
|
||
|
||
interface Props {
|
||
destinations: Destination[];
|
||
}
|
||
|
||
export default function FirstMonthCost({ destinations }: Props) {
|
||
const { t } = useI18n();
|
||
const { toast } = useToast();
|
||
const [slug, setSlug] = useState(destinations[0]?.slug || "chiangmai");
|
||
const [housing, setHousing] = useState<"budget" | "mid" | "nice">("mid");
|
||
const [includeDeposit, setIncludeDeposit] = useState(true);
|
||
|
||
const base = LANDING[slug] || { rent: 4000, depositMult: 1, sim: 100, transfer: 150, misc: 500 };
|
||
const dest = destinations.find((d) => d.slug === slug);
|
||
const rentFactor = housing === "budget" ? 0.75 : housing === "nice" ? 1.35 : 1;
|
||
|
||
const breakdown = useMemo(() => {
|
||
const rent = Math.round(base.rent * rentFactor);
|
||
const deposit = includeDeposit ? Math.round(rent * base.depositMult) : 0;
|
||
const sim = base.sim;
|
||
const transfer = base.transfer;
|
||
const misc = Math.round(base.misc * rentFactor);
|
||
const total = rent + deposit + sim + transfer + misc;
|
||
return [
|
||
{ key: "rent", emoji: "🏡", label: "首月房租", amount: rent },
|
||
{ key: "deposit", emoji: "🔐", label: `押金 ×${base.depositMult}`, amount: deposit },
|
||
{ key: "sim", emoji: "📱", label: "上网卡", amount: sim },
|
||
{ key: "transfer", emoji: "🚕", label: "机场接驳", amount: transfer },
|
||
{ key: "misc", emoji: "🛒", label: "日用启动", amount: misc },
|
||
{ key: "total", emoji: "💳", label: "合计启动金", amount: total },
|
||
];
|
||
}, [base, rentFactor, includeDeposit]);
|
||
|
||
const total = breakdown.find((b) => b.key === "total")!.amount;
|
||
const maxBar = Math.max(...breakdown.filter((b) => b.key !== "total").map((b) => b.amount), 1);
|
||
|
||
const addToPlan = () => {
|
||
if (!dest) return;
|
||
const { added } = mergeDestinationsIntoTrip([dest], 1);
|
||
toast(
|
||
added ? `${dest.emoji} ${dest.name} ${t.plan.addedToPlan}` : t.common.alreadyInPlan,
|
||
added ? "success" : "info",
|
||
{ href: "/plan", label: t.strip.openPlan }
|
||
);
|
||
};
|
||
|
||
return (
|
||
<section className="section firstmonth-section" id="first-month">
|
||
<div className="container">
|
||
<div className="section-header reveal">
|
||
<span className="section-tag">🛬 FIRST MONTH</span>
|
||
<h2>落地首月成本</h2>
|
||
<p>房租、押金、SIM、接机一次算清,避免到了才慌</p>
|
||
</div>
|
||
|
||
<div className="firstmonth-card reveal">
|
||
<div className="firstmonth-controls">
|
||
<label>
|
||
<span>城市</span>
|
||
<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>
|
||
</label>
|
||
<div className="firstmonth-housing">
|
||
{([
|
||
["budget", "💰 经济"],
|
||
["mid", "🏠 舒适"],
|
||
["nice", "✨ 品质"],
|
||
] as const).map(([k, label]) => (
|
||
<button
|
||
key={k}
|
||
type="button"
|
||
className={`firstmonth-chip${housing === k ? " active" : ""}`}
|
||
onClick={() => setHousing(k)}
|
||
>
|
||
{label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
<label className="firstmonth-check">
|
||
<input
|
||
type="checkbox"
|
||
checked={includeDeposit}
|
||
onChange={(e) => setIncludeDeposit(e.target.checked)}
|
||
/>
|
||
<span>计入押金(当地常见 {base.depositMult} 个月)</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div className="firstmonth-result">
|
||
<div className="firstmonth-total">
|
||
<small>{dest?.emoji} 建议准备</small>
|
||
<strong>¥{total.toLocaleString()}</strong>
|
||
<span>含首月硬成本,不含机票与签证费</span>
|
||
</div>
|
||
<div className="firstmonth-bars">
|
||
{breakdown.filter((b) => b.key !== "total" && b.amount > 0).map((b) => (
|
||
<div key={b.key} className="firstmonth-bar-row">
|
||
<span>{b.emoji} {b.label}</span>
|
||
<div className="firstmonth-bar-track">
|
||
<i style={{ width: `${(b.amount / maxBar) * 100}%` }} />
|
||
</div>
|
||
<em>¥{b.amount.toLocaleString()}</em>
|
||
</div>
|
||
))}
|
||
</div>
|
||
{dest && (
|
||
<div className="dating-empty-actions" style={{ marginTop: "1rem" }}>
|
||
<button type="button" className="btn btn-primary btn-sm" onClick={addToPlan}>
|
||
🗓️ {t.nav.plan}
|
||
</button>
|
||
<Link href={`/destinations/${dest.slug}`} className="btn btn-sm">
|
||
{t.common.detail}
|
||
</Link>
|
||
<Link href={meetupCityHref(dest.name)} className="btn btn-sm btn-ghost">
|
||
{t.common.cityMeetups}
|
||
</Link>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|