Fix member profiles, chat peer header, and like-back CTAs.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
5525d04744
commit
fe4f0f5983
@ -14,16 +14,29 @@ from app.services.auth import (
|
|||||||
update_profile,
|
update_profile,
|
||||||
)
|
)
|
||||||
from app.services.pocketbase import pb
|
from app.services.pocketbase import pb
|
||||||
|
from app.services import social_store
|
||||||
|
|
||||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||||
|
|
||||||
|
|
||||||
|
def _with_social_seed(result: dict | None) -> dict | None:
|
||||||
|
if not result:
|
||||||
|
return result
|
||||||
|
try:
|
||||||
|
uid = (result.get("user") or {}).get("id")
|
||||||
|
if uid:
|
||||||
|
social_store.seed_demo_social_activity(uid)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.post("/register", response_model=AuthResponse)
|
@router.post("/register", response_model=AuthResponse)
|
||||||
async def register(body: AuthRegister):
|
async def register(body: AuthRegister):
|
||||||
result = register_user(body.email, body.password, body.name)
|
result = register_user(body.email, body.password, body.name)
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(400, "该邮箱已注册")
|
raise HTTPException(400, "该邮箱已注册")
|
||||||
return AuthResponse(**result)
|
return AuthResponse(**_with_social_seed(result))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/login", response_model=AuthResponse)
|
@router.post("/login", response_model=AuthResponse)
|
||||||
@ -31,7 +44,7 @@ async def login(body: AuthLogin):
|
|||||||
result = login_user(body.email, body.password)
|
result = login_user(body.email, body.password)
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(401, "邮箱或密码错误")
|
raise HTTPException(401, "邮箱或密码错误")
|
||||||
return AuthResponse(**result)
|
return AuthResponse(**_with_social_seed(result))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/me", response_model=UserProfile)
|
@router.get("/me", response_model=UserProfile)
|
||||||
@ -69,7 +82,7 @@ async def demo_login():
|
|||||||
result = login_demo()
|
result = login_demo()
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(500, "演示账号不可用")
|
raise HTTPException(500, "演示账号不可用")
|
||||||
return AuthResponse(**result)
|
return AuthResponse(**_with_social_seed(result))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/google", response_model=AuthResponse)
|
@router.post("/google", response_model=AuthResponse)
|
||||||
@ -92,7 +105,7 @@ async def google_login(body: GoogleLoginRequest):
|
|||||||
result = login_google(email, name)
|
result = login_google(email, name)
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(500, "登录失败")
|
raise HTTPException(500, "登录失败")
|
||||||
return AuthResponse(**result)
|
return AuthResponse(**_with_social_seed(result))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/favorites", response_model=FavoriteResponse)
|
@router.get("/favorites", response_model=FavoriteResponse)
|
||||||
|
|||||||
@ -159,14 +159,84 @@ def join_member(user_id: str, name: str, payload: dict) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def get_public_profile(user_id: str) -> dict | None:
|
def get_public_profile(user_id: str) -> dict | None:
|
||||||
|
if not user_id:
|
||||||
|
return None
|
||||||
|
# Accept /members/user-{id} style links
|
||||||
|
lookup = user_id[5:] if user_id.startswith("user-") else user_id
|
||||||
|
|
||||||
if use_pb():
|
if use_pb():
|
||||||
row = safe_first("profiles", filter=f"userId={q(user_id)}")
|
row = safe_first("profiles", filter=f"userId={q(lookup)}")
|
||||||
if not row:
|
if row:
|
||||||
return None
|
return {
|
||||||
return {**row, "vip": is_vip(user_id)}
|
"id": row.get("id") or f"user-{lookup}",
|
||||||
_jload()
|
"userId": row.get("userId") or lookup,
|
||||||
p = (_j.get("profiles") or {}).get(user_id)
|
"name": row.get("name") or "游民",
|
||||||
return {**p, "vip": is_vip(user_id)} if p else None
|
"location": row.get("location") or "",
|
||||||
|
"citySlug": row.get("citySlug") or "",
|
||||||
|
"gender": row.get("gender") or "",
|
||||||
|
"single": row.get("single") or "",
|
||||||
|
"bio": row.get("bio") or "",
|
||||||
|
"photo": row.get("photo") or "🧑💻",
|
||||||
|
"tags": row.get("tags") or [],
|
||||||
|
"lookingFor": _profile_looking_for(row),
|
||||||
|
"vip": is_vip(lookup),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
_jload()
|
||||||
|
p = (_j.get("profiles") or {}).get(lookup)
|
||||||
|
if p:
|
||||||
|
return {
|
||||||
|
"id": f"user-{lookup}",
|
||||||
|
"userId": lookup,
|
||||||
|
"name": p.get("name") or "游民",
|
||||||
|
"location": p.get("location") or "",
|
||||||
|
"citySlug": p.get("citySlug") or "",
|
||||||
|
"gender": p.get("gender") or "",
|
||||||
|
"single": p.get("single") or "",
|
||||||
|
"bio": p.get("bio") or "",
|
||||||
|
"photo": p.get("photo") or "🧑💻",
|
||||||
|
"tags": p.get("tags") or [],
|
||||||
|
"lookingFor": _profile_looking_for(p),
|
||||||
|
"vip": is_vip(lookup),
|
||||||
|
}
|
||||||
|
|
||||||
|
for cand in CANDIDATE_PROFILES:
|
||||||
|
if cand.get("userId") == lookup or cand.get("id") == lookup or cand.get("id") == user_id:
|
||||||
|
return {
|
||||||
|
"id": cand.get("id") or f"user-{cand.get('userId', lookup)}",
|
||||||
|
"userId": cand.get("userId") or lookup,
|
||||||
|
"name": cand.get("name") or "游民",
|
||||||
|
"location": cand.get("location") or "",
|
||||||
|
"citySlug": cand.get("citySlug") or "",
|
||||||
|
"gender": cand.get("gender") or "",
|
||||||
|
"single": cand.get("single") or "",
|
||||||
|
"bio": cand.get("bio") or "",
|
||||||
|
"photo": cand.get("photo") or "🧑💻",
|
||||||
|
"tags": cand.get("tags") or [],
|
||||||
|
"lookingFor": _profile_looking_for(cand),
|
||||||
|
"vip": False,
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_peer(peer: dict | None) -> dict | None:
|
||||||
|
if not peer:
|
||||||
|
return None
|
||||||
|
uid = peer.get("userId") or ""
|
||||||
|
return {
|
||||||
|
"id": peer.get("id") or (f"user-{uid}" if uid else "unknown"),
|
||||||
|
"userId": uid,
|
||||||
|
"name": peer.get("name") or "游民",
|
||||||
|
"location": peer.get("location") or "",
|
||||||
|
"citySlug": peer.get("citySlug") or "",
|
||||||
|
"gender": peer.get("gender") or "",
|
||||||
|
"single": peer.get("single") or "",
|
||||||
|
"bio": peer.get("bio") or "",
|
||||||
|
"photo": peer.get("photo") or "🧑💻",
|
||||||
|
"tags": peer.get("tags") or [],
|
||||||
|
"lookingFor": _profile_looking_for(peer),
|
||||||
|
"intent": peer.get("intent") or "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ── Candidates / swipes ────────────────────────────────────────────────
|
# ── Candidates / swipes ────────────────────────────────────────────────
|
||||||
@ -511,19 +581,23 @@ def list_conversations(user_id: str) -> list[dict]:
|
|||||||
rows = safe_list("conversations", filter=f"userAId={q(user_id)} || userBId={q(user_id)}")
|
rows = safe_list("conversations", filter=f"userAId={q(user_id)} || userBId={q(user_id)}")
|
||||||
rows.sort(key=lambda c: c.get("lastMessageAt") or c.get("created", ""), reverse=True)
|
rows.sort(key=lambda c: c.get("lastMessageAt") or c.get("created", ""), reverse=True)
|
||||||
for c in rows:
|
for c in rows:
|
||||||
|
if c.get("userAId") == c.get("userBId"):
|
||||||
|
continue
|
||||||
peer_id = c["userBId"] if c["userAId"] == user_id else c["userAId"]
|
peer_id = c["userBId"] if c["userAId"] == user_id else c["userAId"]
|
||||||
read_at = (c.get("readState") or {}).get(user_id, "")
|
read_at = (c.get("readState") or {}).get(user_id, "")
|
||||||
unread = 1 if (c.get("lastMessageAt") or "") > read_at else 0
|
unread = 1 if (c.get("lastMessageAt") or "") > read_at else 0
|
||||||
items.append({**c, "peer": _peer_profile(peer_id), "unreadCount": unread})
|
items.append({**c, "peer": normalize_peer(_peer_profile(peer_id)), "unreadCount": unread})
|
||||||
return items
|
return items
|
||||||
_jload()
|
_jload()
|
||||||
for c in _j.get("conversations") or []:
|
for c in _j.get("conversations") or []:
|
||||||
if user_id not in (c.get("userAId"), c.get("userBId")):
|
if user_id not in (c.get("userAId"), c.get("userBId")):
|
||||||
continue
|
continue
|
||||||
|
if c.get("userAId") == c.get("userBId"):
|
||||||
|
continue
|
||||||
peer_id = c["userBId"] if c["userAId"] == user_id else c["userAId"]
|
peer_id = c["userBId"] if c["userAId"] == user_id else c["userAId"]
|
||||||
read_at = (c.get("readState") or {}).get(user_id, "")
|
read_at = (c.get("readState") or {}).get(user_id, "")
|
||||||
unread = 1 if c.get("lastMessageAt", "") > read_at else 0
|
unread = 1 if c.get("lastMessageAt", "") > read_at else 0
|
||||||
items.append({**c, "peer": _peer_profile(peer_id), "unreadCount": unread})
|
items.append({**c, "peer": normalize_peer(_peer_profile(peer_id)), "unreadCount": unread})
|
||||||
items.sort(key=lambda x: x.get("lastMessageAt", ""), reverse=True)
|
items.sort(key=lambda x: x.get("lastMessageAt", ""), reverse=True)
|
||||||
return items
|
return items
|
||||||
|
|
||||||
@ -534,12 +608,12 @@ def get_conversation(conv_id: str, user_id: str) -> dict | None:
|
|||||||
if not row or user_id not in (row.get("userAId"), row.get("userBId")):
|
if not row or user_id not in (row.get("userAId"), row.get("userBId")):
|
||||||
return None
|
return None
|
||||||
peer_id = row["userBId"] if row["userAId"] == user_id else row["userAId"]
|
peer_id = row["userBId"] if row["userAId"] == user_id else row["userAId"]
|
||||||
return {**row, "peer": _peer_profile(peer_id)}
|
return {**row, "peer": normalize_peer(_peer_profile(peer_id))}
|
||||||
_jload()
|
_jload()
|
||||||
for c in _j.get("conversations") or []:
|
for c in _j.get("conversations") or []:
|
||||||
if c["id"] == conv_id and user_id in (c.get("userAId"), c.get("userBId")):
|
if c["id"] == conv_id and user_id in (c.get("userAId"), c.get("userBId")):
|
||||||
peer_id = c["userBId"] if c["userAId"] == user_id else c["userAId"]
|
peer_id = c["userBId"] if c["userAId"] == user_id else c["userAId"]
|
||||||
return {**c, "peer": _peer_profile(peer_id)}
|
return {**c, "peer": normalize_peer(_peer_profile(peer_id))}
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,14 @@ export const metadata: Metadata = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const LOGS = [
|
const LOGS = [
|
||||||
|
{
|
||||||
|
date: "2026-09-04",
|
||||||
|
tag: "匹配资料 · 私信头",
|
||||||
|
items: [
|
||||||
|
"会员页支持候选档案 /members/u1,可回赞并直达聊天;私信线程显示对方头像与资料入口",
|
||||||
|
"登录即注入匹配来赞与演示私信;会话 peer 字段规范化避免列表 500",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
date: "2026-09-04",
|
date: "2026-09-04",
|
||||||
tag: "匹配滑动 · 喜欢我的",
|
tag: "匹配滑动 · 喜欢我的",
|
||||||
|
|||||||
@ -10876,6 +10876,33 @@ a.weather-card:hover {
|
|||||||
min-height: calc(100vh - 120px);
|
min-height: calc(100vh - 120px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-thread-peer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-bottom: var(--border-glass);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thread-peer-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thread-peer-link strong {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thread-peer-link .dating-meta {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-thread-messages {
|
.chat-thread-messages {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import SiteShell from "@/components/SiteShell";
|
import SiteShell from "@/components/SiteShell";
|
||||||
@ -9,7 +10,9 @@ export default async function MemberPage({ params }: { params: Promise<{ id: str
|
|||||||
if (!profile) notFound();
|
if (!profile) notFound();
|
||||||
return (
|
return (
|
||||||
<SiteShell showFooter>
|
<SiteShell showFooter>
|
||||||
<MemberProfileClient profile={profile} />
|
<Suspense fallback={<div className="container dest-empty">…</div>}>
|
||||||
|
<MemberProfileClient profile={profile} />
|
||||||
|
</Suspense>
|
||||||
</SiteShell>
|
</SiteShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { api } from "@/lib/api";
|
|||||||
import { useAuth } from "@/lib/auth";
|
import { useAuth } from "@/lib/auth";
|
||||||
import { useI18n } from "@/lib/i18n";
|
import { useI18n } from "@/lib/i18n";
|
||||||
import { useToast } from "@/lib/toast";
|
import { useToast } from "@/lib/toast";
|
||||||
import type { ChatMessage } from "@/lib/types";
|
import type { ChatMessage, ConversationItem } from "@/lib/types";
|
||||||
import RingNext from "@/components/RingNext";
|
import RingNext from "@/components/RingNext";
|
||||||
import { useRingSteps } from "@/lib/rings";
|
import { useRingSteps } from "@/lib/rings";
|
||||||
import { clearDraft, loadDraft, saveDraft } from "@/lib/localDraft";
|
import { clearDraft, loadDraft, saveDraft } from "@/lib/localDraft";
|
||||||
@ -17,6 +17,7 @@ export default function ChatThreadClient({ convId }: { convId: string }) {
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const rings = useRingSteps();
|
const rings = useRingSteps();
|
||||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||||
|
const [conv, setConv] = useState<ConversationItem | null>(null);
|
||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("");
|
||||||
const [sending, setSending] = useState(false);
|
const [sending, setSending] = useState(false);
|
||||||
const [loading, setLoading] = useState(Boolean(token));
|
const [loading, setLoading] = useState(Boolean(token));
|
||||||
@ -38,6 +39,7 @@ export default function ChatThreadClient({ convId }: { convId: string }) {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
api.getConversation(token, convId).then(setConv).catch(() => setConv(null));
|
||||||
load();
|
load();
|
||||||
const id = setInterval(load, 4000);
|
const id = setInterval(load, 4000);
|
||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
@ -85,6 +87,9 @@ export default function ChatThreadClient({ convId }: { convId: string }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const peer = conv?.peer;
|
||||||
|
const peerHref = peer?.userId ? `/members/${peer.userId}` : peer?.id ? `/members/${peer.id}` : "/dating";
|
||||||
|
|
||||||
if (!user || !token) {
|
if (!user || !token) {
|
||||||
return (
|
return (
|
||||||
<div className="chat-thread-page">
|
<div className="chat-thread-page">
|
||||||
@ -115,8 +120,26 @@ export default function ChatThreadClient({ convId }: { convId: string }) {
|
|||||||
<nav className="detail-nav">
|
<nav className="detail-nav">
|
||||||
<Link href="/chat">← {t.chat.back}</Link>
|
<Link href="/chat">← {t.chat.back}</Link>
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
|
<Link href="/dating">{t.nav.dating}</Link>
|
||||||
|
<span> · </span>
|
||||||
<Link href="/meetups">{t.nav.meetups}</Link>
|
<Link href="/meetups">{t.nav.meetups}</Link>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
{peer && (
|
||||||
|
<div className="chat-thread-peer reveal">
|
||||||
|
<Link href={peerHref} className="chat-thread-peer-link">
|
||||||
|
<span className="chat-avatar">{peer.photo || peer.name?.[0] || "💬"}</span>
|
||||||
|
<div>
|
||||||
|
<strong>{peer.name}</strong>
|
||||||
|
<p className="dating-meta">{peer.location || t.dating.globalNomad}</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
<Link href={peerHref} className="btn btn-ghost btn-sm">
|
||||||
|
{t.dating.viewProfile}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="chat-thread-messages">
|
<div className="chat-thread-messages">
|
||||||
{loading && messages.length === 0 && <p className="dest-empty">{t.chat.loadingMessages}</p>}
|
{loading && messages.length === 0 && <p className="dest-empty">{t.chat.loadingMessages}</p>}
|
||||||
{!loading && messages.length === 0 && (
|
{!loading && messages.length === 0 && (
|
||||||
|
|||||||
@ -108,7 +108,11 @@ export default function DatingLikesClient() {
|
|||||||
{!loading &&
|
{!loading &&
|
||||||
list.map((p) => {
|
list.map((p) => {
|
||||||
const photo = p.photo || "";
|
const photo = p.photo || "";
|
||||||
const href = p.userId ? `/members/${p.userId}` : "/dating";
|
const href = p.userId
|
||||||
|
? `/members/${p.userId}?from=likes`
|
||||||
|
: p.id
|
||||||
|
? `/members/${p.id}?from=likes`
|
||||||
|
: "/dating";
|
||||||
return (
|
return (
|
||||||
<Link key={p.id || p.userId || p.name} href={href} className="dating-like-card reveal">
|
<Link key={p.id || p.userId || p.name} href={href} className="dating-like-card reveal">
|
||||||
<div className="dating-like-photo">
|
<div className="dating-like-photo">
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { api } from "@/lib/api";
|
||||||
|
import { useAuth } from "@/lib/auth";
|
||||||
import { useI18n } from "@/lib/i18n";
|
import { useI18n } from "@/lib/i18n";
|
||||||
|
import { useToast } from "@/lib/toast";
|
||||||
import type { MemberProfile } from "@/lib/types";
|
import type { MemberProfile } from "@/lib/types";
|
||||||
import RingNext from "@/components/RingNext";
|
import RingNext from "@/components/RingNext";
|
||||||
import { useRingSteps } from "@/lib/rings";
|
import { useRingSteps } from "@/lib/rings";
|
||||||
@ -15,11 +20,59 @@ function cityFromLocation(location?: string) {
|
|||||||
|
|
||||||
export default function MemberProfileClient({ profile }: { profile: MemberProfile }) {
|
export default function MemberProfileClient({ profile }: { profile: MemberProfile }) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const { token, user } = useAuth();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const router = useRouter();
|
||||||
|
const search = useSearchParams();
|
||||||
const rings = useRingSteps();
|
const rings = useRingSteps();
|
||||||
const photo = profile.photo || "";
|
const photo = profile.photo || "";
|
||||||
const isUrl = photo.startsWith("http");
|
const isUrl = photo.startsWith("http");
|
||||||
const city = cityFromLocation(profile.location);
|
const city = cityFromLocation(profile.location);
|
||||||
const meetupsHref = city ? meetupCityHref(city) : "/meetups";
|
const meetupsHref = city ? meetupCityHref(city) : "/meetups";
|
||||||
|
const [convId, setConvId] = useState<string | null>(null);
|
||||||
|
const [liking, setLiking] = useState(false);
|
||||||
|
const fromLikes = search.get("from") === "likes";
|
||||||
|
|
||||||
|
const profileId = profile.id || (profile.userId ? `user-${profile.userId}` : "");
|
||||||
|
const memberHref = profile.userId || profile.id || "";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!token || !profile.userId) return;
|
||||||
|
api
|
||||||
|
.getMutualMatches(token)
|
||||||
|
.then((res) => {
|
||||||
|
const hit = (res.items || []).find(
|
||||||
|
(m) => m.peer?.userId === profile.userId || m.peer?.id === profile.id
|
||||||
|
);
|
||||||
|
if (hit?.conversationId) setConvId(hit.conversationId);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, [token, profile.userId, profile.id]);
|
||||||
|
|
||||||
|
const likeBack = async () => {
|
||||||
|
if (!token) {
|
||||||
|
router.push(`/login?next=/members/${memberHref}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!profileId || liking) return;
|
||||||
|
setLiking(true);
|
||||||
|
try {
|
||||||
|
const res = await api.swipe(token, profileId, "like", "friends");
|
||||||
|
if (res.matched && res.match?.conversationId) {
|
||||||
|
setConvId(res.match.conversationId);
|
||||||
|
toast(t.dating.matched + " " + profile.name, "success", {
|
||||||
|
href: `/chat/${res.match.conversationId}`,
|
||||||
|
label: t.dating.chatNow,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast(t.dating.likeSent, "success", { href: "/dating/likes", label: t.dating.likesTitle });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast(t.dating.swipeFail, "error");
|
||||||
|
} finally {
|
||||||
|
setLiking(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dating-page">
|
<div className="dating-page">
|
||||||
@ -27,6 +80,8 @@ export default function MemberProfileClient({ profile }: { profile: MemberProfil
|
|||||||
<nav className="detail-nav">
|
<nav className="detail-nav">
|
||||||
<Link href="/dating">← {t.nav.dating}</Link>
|
<Link href="/dating">← {t.nav.dating}</Link>
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
|
<Link href="/dating/likes">{t.dating.likesTitle}</Link>
|
||||||
|
<span> · </span>
|
||||||
<Link href="/chat">{t.nav.chat}</Link>
|
<Link href="/chat">{t.nav.chat}</Link>
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
<Link href={meetupsHref}>{t.nav.meetups}</Link>
|
<Link href={meetupsHref}>{t.nav.meetups}</Link>
|
||||||
@ -53,6 +108,7 @@ export default function MemberProfileClient({ profile }: { profile: MemberProfil
|
|||||||
profile.location || t.dating.globalNomad
|
profile.location || t.dating.globalNomad
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
{fromLikes && <p className="meetup-tag">{t.dating.likesYou}</p>}
|
||||||
<p>{profile.bio || t.dating.noBio}</p>
|
<p>{profile.bio || t.dating.noBio}</p>
|
||||||
<div className="dating-tags">
|
<div className="dating-tags">
|
||||||
{(profile.tags || []).map((tag) => (
|
{(profile.tags || []).map((tag) => (
|
||||||
@ -62,15 +118,25 @@ export default function MemberProfileClient({ profile }: { profile: MemberProfil
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="dating-empty-actions" style={{ marginTop: "1.25rem" }}>
|
<div className="dating-empty-actions" style={{ marginTop: "1.25rem" }}>
|
||||||
<Link href="/dating" className="btn btn-primary btn-sm">
|
{convId ? (
|
||||||
|
<Link href={`/chat/${convId}`} className="btn btn-primary btn-sm">
|
||||||
|
{t.dating.chatNow}
|
||||||
|
</Link>
|
||||||
|
) : user ? (
|
||||||
|
<button type="button" className="btn btn-primary btn-sm" disabled={liking} onClick={() => void likeBack()}>
|
||||||
|
{liking ? "…" : t.dating.likeBack}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<Link href={`/login?next=/members/${memberHref}`} className="btn btn-primary btn-sm">
|
||||||
|
{t.nav.login}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
<Link href="/dating" className="btn btn-sm">
|
||||||
{t.dating.keepSwiping}
|
{t.dating.keepSwiping}
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={meetupsHref} className="btn btn-sm">
|
<Link href={meetupsHref} className="btn btn-sm">
|
||||||
{city ? t.common.cityMeetups : t.dating.meetAtEvents}
|
{city ? t.common.cityMeetups : t.dating.meetAtEvents}
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/community" className="btn btn-sm">
|
|
||||||
{t.nav.community}
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<RingNext steps={rings.afterDating} />
|
<RingNext steps={rings.afterDating} />
|
||||||
|
|||||||
@ -250,6 +250,15 @@ export const api = {
|
|||||||
}),
|
}),
|
||||||
getConversations: (token: string) =>
|
getConversations: (token: string) =>
|
||||||
fetchAPI<ConversationItem[]>("/social/conversations", { headers: { Authorization: `Bearer ${token}` } }),
|
fetchAPI<ConversationItem[]>("/social/conversations", { headers: { Authorization: `Bearer ${token}` } }),
|
||||||
|
getConversation: (token: string, convId: string) =>
|
||||||
|
fetchAPI<ConversationItem>(`/social/conversations/${convId}`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
}),
|
||||||
|
getMutualMatches: (token: string) =>
|
||||||
|
fetchAPI<{ items: Array<{ conversationId?: string; peer?: MatchProfile; intent?: string }> }>(
|
||||||
|
"/social/matches/mutual",
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
),
|
||||||
getMessages: (token: string, convId: string) =>
|
getMessages: (token: string, convId: string) =>
|
||||||
fetchAPI<ChatMessage[]>(`/social/conversations/${convId}/messages`, {
|
fetchAPI<ChatMessage[]>(`/social/conversations/${convId}/messages`, {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
|||||||
@ -457,6 +457,9 @@ export const zh = {
|
|||||||
likesReceivedSubtitle: "对你右滑的游民会出现在这里",
|
likesReceivedSubtitle: "对你右滑的游民会出现在这里",
|
||||||
likesReceivedEmpty: "还没有人喜欢你,先去完善资料并滑动匹配",
|
likesReceivedEmpty: "还没有人喜欢你,先去完善资料并滑动匹配",
|
||||||
likesYou: "喜欢了你",
|
likesYou: "喜欢了你",
|
||||||
|
likeBack: "回赞 / 匹配",
|
||||||
|
likeSent: "已发送喜欢",
|
||||||
|
viewProfile: "查看资料",
|
||||||
},
|
},
|
||||||
chat: {
|
chat: {
|
||||||
tag: "✉️ MESSAGES",
|
tag: "✉️ MESSAGES",
|
||||||
@ -1722,6 +1725,9 @@ export const en: { [K in keyof typeof zh]: typeof zh[K] extends string ? string
|
|||||||
likesReceivedSubtitle: "People who swiped right on you",
|
likesReceivedSubtitle: "People who swiped right on you",
|
||||||
likesReceivedEmpty: "No incoming likes yet — keep your profile fresh and swipe",
|
likesReceivedEmpty: "No incoming likes yet — keep your profile fresh and swipe",
|
||||||
likesYou: "Liked you",
|
likesYou: "Liked you",
|
||||||
|
likeBack: "Like back",
|
||||||
|
likeSent: "Like sent",
|
||||||
|
viewProfile: "View profile",
|
||||||
},
|
},
|
||||||
chat: {
|
chat: {
|
||||||
tag: "✉️ MESSAGES",
|
tag: "✉️ MESSAGES",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user