Files
no-whatever/src/components/ProfileHistoryCard.tsx
T
kurihada 76349f0dcf feat: 接入全站图片资源 + 修复卡片滑动与房间轮询问题
图片资源接入:
- OG/Twitter 社交分享元数据 (og-image.png)
- 错误页插画替换图标 (error-robot.png)
- EmptyState 组件支持 image prop,空状态页面接入插画
- 餐厅图片 fallback 改用 restaurant-fallback.png
- 极速救场/周末契约页面添加 hero 装饰图
- 分享卡片添加背景图层 (share-bg-*.png),通过 base64 预加载
- 更新 App 图标 (apple-touch-icon, icon-192/512)

Bug 修复:
- SwipeDeck: swipe action 从 "nope" 改为 "pass",匹配 API 预期
- SwipeDeck: 用 ref 读取 currentIndex 避免竞态重置(本地滑动后
  被服务端旧 swipeCounts 立即清零)
- SwipeDeck: 卡片 key 加入 isTop 标识,强制 remount 解决
  framer-motion drag 手势在 isTop 切换时不重新初始化的问题
- SwipeableCard: initial 统一为背景位置,确保晋升为顶部卡片时
  有一致的放大动画
- useRoomPolling: roomId 为空时跳过 SWR 和 EventSource
- room page: joinRoom 前 guard roomId,消除退房时 404
- layout: 添加 metadataBase 消除 Next.js OG 图片警告
2026-02-27 16:08:38 +08:00

115 lines
4.0 KiB
TypeScript

"use client";
import { motion, AnimatePresence } from "framer-motion";
import { Clock, ChevronDown, ClipboardList } from "lucide-react";
import Card from "@/components/Card";
import EmptyState from "@/components/EmptyState";
import RestaurantImage from "@/components/RestaurantImage";
import { RecordItemSkeleton } from "@/components/Skeleton";
import { buildNavUrl } from "@/lib/navigation";
import type { DecisionRecord, Restaurant } from "@/types";
function firstImage(r: Restaurant): string {
if (r.images?.length > 0) return r.images[0];
const legacy = (r as unknown as Record<string, unknown>).image;
return typeof legacy === "string" ? legacy : "";
}
interface ProfileHistoryCardProps {
history: DecisionRecord[];
loading: boolean;
open: boolean;
onToggle: () => void;
onEmpty: () => void;
delay?: number;
}
export default function ProfileHistoryCard({
history,
loading,
open,
onToggle,
onEmpty,
delay,
}: ProfileHistoryCardProps) {
return (
<Card animated className="mt-4" delay={delay}>
<button
onClick={onToggle}
className="flex w-full items-center justify-between"
>
<div className="flex items-center gap-2">
<Clock size={15} className="text-muted" />
<h3 className="text-sm font-semibold text-secondary">
{history.length > 0 && `(${history.length})`}
</h3>
</div>
<motion.span
animate={{ rotate: open ? 180 : 0 }}
transition={{ duration: 0.2 }}
className="text-muted"
>
<ChevronDown size={16} />
</motion.span>
</button>
<AnimatePresence>
{open && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
{loading ? (
<div className="mt-3 flex flex-col gap-2">
<RecordItemSkeleton />
<RecordItemSkeleton />
</div>
) : history.length === 0 ? (
<EmptyState
icon={ClipboardList}
image="/empty-no-record.png"
title="还没有决策记录"
subtitle="创建房间开始一起选餐厅"
ctaLabel="去创建第一个房间"
onCta={onEmpty}
color="purple"
/>
) : (
<div className="mt-3 flex flex-col gap-2">
{history.map((d) => (
<a
key={d.id}
href={buildNavUrl(d.restaurantData)}
target="_blank"
rel="noopener noreferrer"
className="flex gap-3 rounded-xl bg-elevated p-2.5 transition-colors active:bg-subtle"
>
{firstImage(d.restaurantData) && (
<RestaurantImage
src={firstImage(d.restaurantData)}
alt={d.restaurantName}
className="h-12 w-12 shrink-0 rounded-lg object-cover"
/>
)}
<div className="flex min-w-0 flex-1 flex-col justify-center">
<p className="truncate text-sm font-semibold text-heading">{d.restaurantName}</p>
<div className="mt-0.5 flex items-center gap-2 text-[11px] text-muted">
<span>{d.matchType === "unanimous" ? "全员一致" : "最佳匹配"}</span>
<span>{d.participants} </span>
<span>{new Date(d.createdAt).toLocaleDateString("zh-CN", { month: "short", day: "numeric" })}</span>
</div>
</div>
</a>
))}
</div>
)}
</motion.div>
)}
</AnimatePresence>
</Card>
);
}