Files
gitlab-instance-0a899031_cn…/app/[locale]/chat/page.tsx
2026-06-08 03:06:12 -05:00

160 lines
6.4 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { Link } from "@/i18n/navigation";
import { useLocale, useTranslation } from "@/i18n/navigation";
import Footer from "@/app/components/Footer";
import { fetchAuthMe } from "@/app/lib/api-client";
import { fetchConversations, type ConversationItem } from "@/app/lib/chat";
import { mediaUrl } from "@/app/lib/media";
function formatTime(value?: string, locale = "zh"): string {
if (!value) return "";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "";
const diff = Date.now() - date.getTime();
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return locale === "zh" ? "刚刚" : "Now";
if (minutes < 60) return locale === "zh" ? `${minutes} 分钟前` : `${minutes}m`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return locale === "zh" ? `${hours} 小时前` : `${hours}h`;
const days = Math.floor(hours / 24);
if (days < 7) return locale === "zh" ? `${days} 天前` : `${days}d`;
return `${date.getMonth() + 1}/${date.getDate()}`;
}
function ConversationRow({ item, locale }: { item: ConversationItem; locale: string }) {
const peerName = item.peer?.name || (locale === "zh" ? "成员" : "Member");
const preview = item.lastMessagePreview || (locale === "zh" ? "开始聊天吧" : "Say hello");
const unread = Number(item.unreadCount || 0);
return (
<Link
href={`/chat/${item.id}`}
className="flex items-center gap-3 rounded-2xl border border-gray-100 bg-white p-4 transition hover:border-[#ff4d4f]/30 hover:shadow-md dark:border-gray-800 dark:bg-gray-900 dark:hover:border-[#ff4d4f]/40"
>
<div className="relative flex-shrink-0">
<img
src={mediaUrl(item.peer?.photo)}
alt={peerName}
className="h-14 w-14 rounded-full object-cover shadow-sm"
/>
{unread > 0 ? (
<span className="absolute -right-1 -top-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-[#ff4d4f] px-1 text-[10px] font-bold text-white">
{unread > 9 ? "9+" : unread}
</span>
) : null}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-2">
<p className="truncate font-semibold text-gray-900 dark:text-gray-100">{peerName}</p>
<span className="flex-shrink-0 text-[11px] text-gray-400">{formatTime(item.lastMessageAt, locale)}</span>
</div>
<p className="mt-1 truncate text-sm text-gray-500 dark:text-gray-400">
{item.peer?.location ? `📍 ${item.peer.location} · ` : ""}
{preview}
</p>
</div>
</Link>
);
}
export default function ChatListPage() {
const { t } = useTranslation("chat");
const locale = useLocale();
const [items, setItems] = useState<ConversationItem[]>([]);
const [unread, setUnread] = useState(0);
const [loading, setLoading] = useState(true);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const load = () => {
setLoading(true);
fetchConversations()
.then((data) => {
setItems(data.items);
setUnread(data.unread);
})
.catch(() => {
setItems([]);
setUnread(0);
})
.finally(() => setLoading(false));
};
useEffect(() => {
fetchAuthMe().then((data) => setIsLoggedIn(Boolean(data?.user?.id)));
load();
}, []);
return (
<div className="min-h-screen bg-[#f6f7f9] dark:bg-gray-950">
<main className="mx-auto max-w-3xl px-4 py-8 sm:px-6 sm:py-10">
<header className="mb-6">
<p className="text-sm font-semibold text-[#ff4d4f]">Direct Messages</p>
<div className="mt-2 flex flex-wrap items-end justify-between gap-3">
<div>
<h1 className="text-3xl font-bold text-gray-950 dark:text-gray-50">{t("title")}</h1>
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">{t("subtitle")}</p>
</div>
<div className="flex gap-2">
<Link
href="/messages"
className="rounded-xl border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-200"
>
{t("notifications")}
</Link>
<Link
href="/dating"
className="rounded-xl bg-[#ff4d4f] px-4 py-2 text-sm font-semibold text-white hover:bg-red-600"
>
{t("goMatching")}
</Link>
</div>
</div>
{unread > 0 ? (
<p className="mt-3 text-sm font-medium text-[#ff4d4f]">
{locale === "zh" ? `${unread} 条未读私信` : `${unread} unread message${unread === 1 ? "" : "s"}`}
</p>
) : null}
</header>
{!isLoggedIn ? (
<div className="rounded-2xl border border-dashed border-gray-200 bg-white p-8 text-center dark:border-gray-800 dark:bg-gray-900">
<p className="text-gray-700 dark:text-gray-200">{t("loginRequired")}</p>
<Link
href="/join"
className="mt-4 inline-flex rounded-full bg-[#ff4d4f] px-5 py-2.5 text-sm font-semibold text-white hover:bg-red-600"
>
{t("loginAction")}
</Link>
</div>
) : loading ? (
<div className="space-y-3">
{[1, 2, 3].map((key) => (
<div key={key} className="h-20 animate-pulse rounded-2xl bg-gray-200 dark:bg-gray-800" />
))}
</div>
) : items.length ? (
<div className="space-y-3">
{items.map((item) => (
<ConversationRow key={item.id} item={item} locale={locale} />
))}
</div>
) : (
<div className="rounded-2xl border border-dashed border-gray-200 bg-white p-8 text-center dark:border-gray-800 dark:bg-gray-900">
<p className="text-lg font-semibold text-gray-900 dark:text-gray-100">{t("emptyTitle")}</p>
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">{t("emptySubtitle")}</p>
<Link
href="/dating"
className="mt-5 inline-flex rounded-full bg-[#ff4d4f] px-5 py-2.5 text-sm font-semibold text-white hover:bg-red-600"
>
{t("goMatching")}
</Link>
</div>
)}
</main>
<Footer />
</div>
);
}