160 lines
6.4 KiB
TypeScript
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>
|
|
);
|
|
}
|