Files
gitlab-instance-0a899031_cn…/app/[locale]/tools/page.tsx
2026-06-06 04:29:46 -05:00

218 lines
9.5 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 { useEffect, useMemo, useState } from "react";
import { useTranslation } from "@/i18n/navigation";
import Footer from "@/app/components/Footer";
import { apiFetch } from "@/app/lib/api-client";
interface ToolCity {
id: string;
name: string;
temperature: number;
humidity: number;
costPerMonth: number;
internetSpeed: number;
}
const RATES = [
{ from: "CNY", to: "USD", rate: 0.14 },
{ from: "CNY", to: "EUR", rate: 0.13 },
{ from: "CNY", to: "THB", rate: 5.2 },
{ from: "USD", to: "CNY", rate: 7.2 },
];
export default function ToolsPage() {
const { t } = useTranslation("tools");
const [income, setIncome] = useState(100000);
const [cnyAmount, setCnyAmount] = useState(10000);
const [visaDate, setVisaDate] = useState("2025-06-15");
const [monthlySpend, setMonthlySpend] = useState(6000);
const [targetTemp, setTargetTemp] = useState(20);
const [cities, setCities] = useState<ToolCity[]>([]);
const [today] = useState(() => Date.now());
useEffect(() => {
apiFetch<{ items: ToolCity[] }>("/api/cities")
.then((data) => setCities(data.items || []))
.catch(() => setCities([]));
}, []);
const taxEstimate = Math.round(income * 0.15);
const cnyToUsd = (v: number) => (v * 0.14).toFixed(2);
const daysLeft = Math.ceil(
(new Date(visaDate).getTime() - today) / (1000 * 60 * 60 * 24)
);
const fireNumber = monthlySpend * 12 * 25;
const yearsToFire = Math.max(1, Math.ceil(fireNumber / Math.max(1, income - monthlySpend * 12)));
const climateMatches = useMemo(
() =>
[...cities]
.sort((a, b) => Math.abs(a.temperature - targetTemp) - Math.abs(b.temperature - targetTemp))
.slice(0, 5),
[cities, targetTemp]
);
return (
<div className="min-h-screen bg-[#fafafa] dark:bg-gray-950">
<div className="max-w-4xl mx-auto px-4 sm:px-6 py-12">
<div className="mb-10">
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-gray-100">
{t("title")}
</h1>
<p className="mt-1 text-gray-500 dark:text-gray-400">
{t("subtitle")}
</p>
</div>
<div className="space-y-6">
<section className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
💱 {t("exchange")}
</h2>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
{RATES.map((r) => (
<div
key={`${r.from}-${r.to}`}
className="p-4 rounded-xl bg-gray-50 dark:bg-gray-800"
>
<p className="text-xs text-gray-500 dark:text-gray-400">
{r.from} {r.to}
</p>
<p className="mt-1 text-lg font-bold text-gray-900 dark:text-gray-100">
{r.rate}
</p>
</div>
))}
</div>
<div className="mt-4 p-4 rounded-xl bg-gray-50 dark:bg-gray-800">
<label className="block text-xs text-gray-500 dark:text-gray-400 mb-2">CNY USD</label>
<div className="flex gap-2 items-center">
<input
type="number"
value={cnyAmount}
onChange={(e) => setCnyAmount(Number(e.target.value) || 0)}
className="flex-1 rounded-lg border border-gray-200 dark:border-gray-700 px-3 py-2 text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
/>
<span className="text-gray-400"></span>
<span className="font-semibold text-gray-900 dark:text-gray-100">${cnyToUsd(cnyAmount)}</span>
</div>
</div>
<p className="mt-3 text-xs text-gray-400 dark:text-gray-500">
{t("disclaimer")}
</p>
</section>
<section className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
📊 {t("tax")}
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("incomeCountry")} ( ¥)
</label>
<input
type="number"
value={income}
onChange={(e) => setIncome(Number(e.target.value))}
className="w-full rounded-lg border border-gray-200 dark:border-gray-700 px-4 py-2 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
/>
</div>
<div className="p-4 rounded-xl bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800/50">
<p className="text-sm text-amber-800 dark:text-amber-200">
{t("estimate")}: ¥{taxEstimate.toLocaleString()}/
</p>
<p className="mt-1 text-xs text-amber-600 dark:text-amber-300">
{t("taxDisclaimer")}
</p>
</div>
</div>
</section>
<section className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
🛂 {t("visa")}
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("visaExpiry")}
</label>
<input
type="date"
value={visaDate}
onChange={(e) => setVisaDate(e.target.value)}
className="w-full rounded-lg border border-gray-200 dark:border-gray-700 px-4 py-2 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
/>
</div>
<div className="p-4 rounded-xl bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-200 dark:border-emerald-800/50">
<p className="text-lg font-semibold text-emerald-800 dark:text-emerald-200">
{daysLeft > 0 ? `${daysLeft} ${t("daysLeft")}` : t("expired")}
</p>
</div>
</div>
</section>
<section className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
🌱 {t("fireCalculator")}
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">{t("fireDesc")}</p>
<div className="grid gap-4 sm:grid-cols-[1fr_220px]">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
(¥)
</label>
<input
type="number"
value={monthlySpend}
onChange={(e) => setMonthlySpend(Number(e.target.value) || 0)}
className="w-full rounded-lg border border-gray-200 dark:border-gray-700 px-4 py-2 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
/>
</div>
<div className="rounded-xl bg-gray-50 p-4 text-sm dark:bg-gray-800">
<p className="text-gray-500 dark:text-gray-400">FIRE </p>
<p className="mt-1 text-xl font-bold text-gray-900 dark:text-gray-100">¥{fireNumber.toLocaleString()}</p>
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400"> 4% {yearsToFire} </p>
</div>
</div>
</section>
<section className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
🌤 {t("climateFinder")}
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">{t("climateDesc")}</p>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{targetTemp}°C
</label>
<input
type="range"
min={10}
max={30}
value={targetTemp}
onChange={(e) => setTargetTemp(Number(e.target.value))}
className="w-full accent-[#ff4d4f]"
/>
</div>
<div className="grid gap-3 sm:grid-cols-2">
{climateMatches.map((city) => (
<div key={city.id} className="rounded-xl bg-gray-50 p-4 text-sm dark:bg-gray-800">
<p className="font-semibold text-gray-900 dark:text-gray-100">{city.name}</p>
<p className="mt-1 text-gray-500 dark:text-gray-400">
{city.temperature}°C · 湿 {city.humidity}% · ¥{city.costPerMonth.toLocaleString()}/ · {city.internetSpeed}Mbps
</p>
</div>
))}
</div>
</div>
</section>
</div>
</div>
<Footer />
</div>
);
}