Files
gitlab-instance-0a899031_cn…/app/components/CityCard.tsx
2026-03-07 12:33:14 -06:00

190 lines
5.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.
"use client";
import { useState } from "react";
import { City } from "../data/cities";
function ratingToPercent(rating: string): number {
const m: Record<string, number> = {
极好: 95,
很好: 80,
: 60,
一般: 40,
: 20,
};
return m[rating] || 50;
}
function getBarColor(p: number): string {
if (p >= 60) return "#22c55e";
if (p >= 35) return "#eab308";
return "#ef4444";
}
function ScoreBar({
icon,
label,
percent,
animate,
}: {
icon: string;
label: string;
percent: number;
animate: boolean;
}) {
return (
<div className="flex items-center gap-2 sm:gap-3">
<span className="text-white/80 text-xs sm:text-sm w-14 sm:w-16 shrink-0 flex items-center gap-1">
<span>{icon}</span>
<span className="hidden sm:inline">{label}</span>
</span>
<div className="flex-1 h-2 sm:h-2.5 bg-white/10 rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all duration-700 ease-out"
style={{
width: animate ? `${percent}%` : "0%",
backgroundColor: getBarColor(percent),
}}
/>
</div>
</div>
);
}
export default function CityCard({
city,
rank,
}: {
city: City;
rank: number;
}) {
const [hovered, setHovered] = useState(false);
const scores = [
{
icon: "⭐",
label: "综合",
percent: (city.overallScore / 5) * 100,
},
{
icon: "💵",
label: "费用",
percent: Math.max(15, 100 - city.costPerMonth / 200),
},
{
icon: "📡",
label: "网速",
percent: Math.min(95, (city.internetSpeed / 120) * 100),
},
{
icon: "👍",
label: "好评",
percent: ratingToPercent(city.liked),
},
{
icon: "👮",
label: "安全",
percent: ratingToPercent(city.safety),
},
];
return (
<div
className="city-card"
style={{
background: `linear-gradient(135deg, ${city.gradientFrom}, ${city.gradientTo})`,
}}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
{/* Icon watermark */}
<span className="absolute bottom-8 right-2 text-[70px] sm:text-[90px] opacity-[0.12] select-none pointer-events-none leading-none">
{city.icon}
</span>
{/* ===== Default state ===== */}
<div
className={`absolute inset-0 z-10 flex flex-col justify-between p-3 sm:p-4 transition-opacity duration-300 ${hovered ? "opacity-0" : "opacity-100"}`}
>
{/* Top row */}
<div className="flex items-start justify-between">
<span className="text-white/70 text-base sm:text-xl font-bold">
{rank}
</span>
<span className="text-[10px] sm:text-xs bg-white/20 backdrop-blur-sm rounded-full px-1.5 sm:px-2 py-0.5 sm:py-1 text-white flex items-center gap-0.5">
📡 {city.internetSpeed}Mbps
</span>
</div>
{/* Center: city name + country */}
<div className="text-center -mt-2">
<h3 className="text-xl sm:text-[28px] font-bold text-white drop-shadow-md tracking-wide">
{city.name}
</h3>
<p className="text-xs sm:text-sm text-white/60 mt-0.5">
{city.countryEmoji} {city.country}
</p>
</div>
{/* Bottom: temp + cost + nomads */}
<div>
<div className="flex items-center gap-1 mb-1.5">
<span className="text-[9px] sm:text-[11px] bg-white/15 backdrop-blur-sm rounded-full px-1.5 py-0.5 text-white/80">
👥 {city.nomadsNow}
</span>
<span className="text-[9px] sm:text-[11px] bg-white/15 backdrop-blur-sm rounded-full px-1.5 py-0.5 text-white/80">
🌡 {city.temperature}°C
</span>
</div>
<div className="flex items-end justify-between text-white text-xs sm:text-sm">
<span className="text-white/80 text-[10px] sm:text-xs line-clamp-1 max-w-[60%]">
{city.description}
</span>
<span className="font-semibold shrink-0">
¥{city.costPerMonth.toLocaleString()}/
</span>
</div>
</div>
</div>
{/* ===== Hover overlay (desktop only) ===== */}
<div
className={`absolute inset-0 z-20 bg-[#2a2a2a]/95 hidden sm:flex flex-col justify-between p-4 sm:p-5 transition-opacity duration-300 ${hovered ? "opacity-100" : "opacity-0 pointer-events-none"}`}
>
{/* Top icons */}
<div className="flex items-center justify-between">
<button className="text-white/50 hover:text-red-400 transition-colors text-base">
</button>
<button
className="text-white/30 hover:text-white text-sm transition-colors"
onClick={(e) => {
e.stopPropagation();
setHovered(false);
}}
>
</button>
</div>
{/* Score bars */}
<div className="space-y-2.5 sm:space-y-3">
{scores.map((s) => (
<ScoreBar
key={s.label}
icon={s.icon}
label={s.label}
percent={s.percent}
animate={hovered}
/>
))}
</div>
{/* Bottom: city info small */}
<div className="text-center text-white/40 text-xs">
{city.name} · {city.countryEmoji} {city.country}
</div>
</div>
</div>
);
}