Files
2026-03-09 00:05:00 -05:00

218 lines
9.2 KiB
TypeScript
Raw Permalink 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 { useState, useMemo } from "react";
import { useTranslation } from "@/i18n/navigation";
import { Link } from "@/i18n/navigation";
import { cities } from "@/app/data/cities";
import CityCard from "@/app/components/CityCard";
import CityModal from "@/app/components/CityModal";
import Footer from "@/app/components/Footer";
import type { City } from "@/app/data/cities";
const TAGS = ["远程工作者", "创业者", "设计师", "开发者", "内容创作者", "带宠物"];
const TIMEZONES = ["UTC+8 中国", "UTC+7 东南亚", "UTC+1 欧洲", "UTC-5 美东", "UTC-8 美西", "任意"];
const VISA_OPTIONS = ["免签优先", "落地签可", "需提前办", "任意"];
export default function DashboardPage() {
const { t } = useTranslation("dashboard");
const [budget, setBudget] = useState(5000);
const [internet, setInternet] = useState(50);
const [timezone, setTimezone] = useState("UTC+8 中国");
const [visa, setVisa] = useState("任意");
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [cityModal, setCityModal] = useState<City | null>(null);
const filteredCities = useMemo(() => {
return [...cities]
.filter((c) => c.costPerMonth <= budget * 1.2 && c.internetSpeed >= internet)
.sort((a, b) => b.overallScore - a.overallScore)
.slice(0, 12);
}, [budget, internet]);
const toggleTag = (tag: string) => {
setSelectedTags((prev) =>
prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]
);
};
return (
<div className="min-h-screen bg-[#fafafa] dark:bg-gray-950">
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-8 sm:py-12">
<div className="mb-8">
<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="grid grid-cols-1 lg:grid-cols-3 gap-8">
<aside className="lg:col-span-1 space-y-6">
<div className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-4 sm:p-6">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("budget")}
</label>
<input
type="range"
min="2000"
max="15000"
step="500"
value={budget}
onChange={(e) => setBudget(Number(e.target.value))}
className="w-full accent-[#ff4d4f]"
/>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
¥{budget.toLocaleString()}
</p>
</div>
<div className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-4 sm:p-6">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("timezone")}
</label>
<select
value={timezone}
onChange={(e) => setTimezone(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"
>
{TIMEZONES.map((z) => (
<option key={z} value={z}>{z}</option>
))}
</select>
</div>
<div className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-4 sm:p-6">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("visa")}
</label>
<select
value={visa}
onChange={(e) => setVisa(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"
>
{VISA_OPTIONS.map((v) => (
<option key={v} value={v}>{v}</option>
))}
</select>
</div>
<div className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-4 sm:p-6">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("internet")}
</label>
<input
type="range"
min="30"
max="150"
step="10"
value={internet}
onChange={(e) => setInternet(Number(e.target.value))}
className="w-full accent-[#ff4d4f]"
/>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
{internet} Mbps
</p>
</div>
<div className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-4 sm:p-6">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
{t("tags")}
</label>
<div className="flex flex-wrap gap-2">
{TAGS.map((tag) => (
<button
key={tag}
onClick={() => toggleTag(tag)}
className={`px-3 py-1.5 rounded-full text-xs font-medium transition-colors ${
selectedTags.includes(tag)
? "bg-[#ff4d4f] text-white"
: "bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700"
}`}
>
{tag}
</button>
))}
</div>
</div>
<div className="rounded-2xl bg-gradient-to-br from-amber-50 to-orange-50 dark:from-amber-950/30 dark:to-orange-950/30 border border-amber-200 dark:border-amber-800/50 p-4">
<p className="text-sm font-medium text-amber-800 dark:text-amber-200">
🤖 {t("aiTip")}
</p>
<p className="mt-1 text-xs text-amber-700 dark:text-amber-300">
80% 3
</p>
</div>
</aside>
<main className="lg:col-span-2">
<div className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-4 sm:p-6 mb-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
{t("search")}
</h2>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3" style={{ gridAutoRows: "160px" }}>
{filteredCities.map((city, i) => (
<div key={city.id} className="h-full min-h-[140px]">
<CityCard
city={city}
rank={i + 1}
onClick={() => setCityModal(city)}
/>
</div>
))}
</div>
{filteredCities.length === 0 && (
<p className="text-center py-8 text-gray-500 dark:text-gray-400">
{t("noMatch")}
</p>
)}
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-4 h-40 flex items-center justify-center">
<span className="text-gray-400 dark:text-gray-500 text-sm">
{t("costCurve")} {t("devSuffix")}
</span>
</div>
<div className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-4 h-40 flex items-center justify-center">
<span className="text-gray-400 dark:text-gray-500 text-sm">
{t("nomadHeatmap")} {t("devSuffix")}
</span>
</div>
</div>
<div className="mt-6 rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-4">
<h3 className="font-medium text-gray-900 dark:text-gray-100 mb-2">
{t("nearbyEvents")}
</h3>
<div className="flex gap-2 overflow-x-auto pb-2">
{[
{ city: "大理", date: "3月9日", emoji: "🏯" },
{ city: "成都", date: "3月12日", emoji: "🐼" },
{ city: "深圳", date: "3月14日", emoji: "🌃" },
].map((e) => (
<Link
key={e.city}
href="/meetups"
className="shrink-0 px-4 py-2 rounded-xl bg-gray-100 dark:bg-gray-800 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700"
>
{e.emoji} {e.city} {e.date}
</Link>
))}
</div>
</div>
</main>
</div>
</div>
<CityModal
city={cityModal}
isOpen={!!cityModal}
onClose={() => setCityModal(null)}
/>
<Footer />
</div>
);
}