Route meetup RSVP to live/calendar and add Connect stats on profile.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
eric 2026-09-04 06:51:44 -05:00
parent fe4f0f5983
commit cc05691e9a
6 changed files with 113 additions and 7 deletions

View File

@ -275,15 +275,35 @@ async def rsvp_meetup(body: MeetupRsvpRequest, authorization: str | None = Heade
raise HTTPException(400, "活动已满员")
meetup = community_store.get_meetup(body.meetup_id) or {}
title = meetup.get("title") or "活动"
mode = meetup.get("mode") or ""
organizer_id = meetup.get("organizer_id") or ""
if organizer_id and organizer_id != user_id:
from app.services.notify import notify_user
from app.services.notify import notify_user
if organizer_id and organizer_id != user_id:
notify_user(
organizer_id,
"新的活动报名",
f"{user.get('name', '游民')} 报名了「{meetup.get('title', '活动')}」",
f"/meetups",
f"{user.get('name', '游民')} 报名了「{title}」",
"/meetups",
"meetup",
)
# Confirm to attendee with next-step deep link (live room or profile calendar)
if mode in ("online", "hybrid"):
notify_user(
user_id,
"报名成功",
f"你已报名「{title}」,活动开始时可进入直播间",
f"/meetups/{body.meetup_id}/live",
"meetup",
)
else:
notify_user(
user_id,
"报名成功",
f"你已报名「{title}」,可在个人中心导出日历",
"/profile",
"meetup",
)
return {"success": True, "message": res["message"], "rsvp_count": res["rsvp_count"]}

View File

@ -8,6 +8,14 @@ export const metadata: Metadata = {
};
const LOGS = [
{
date: "2026-09-04",
tag: "活动报名 · Connect 数据",
items: [
"报名成功 toast:线上/混合直达直播间,线下引导导出日历;报名确认写入通知箱",
"个人中心新增 Connect 数据条(喜欢我的 / 互相喜欢 / 私信 / 已报名),可点进对应页面",
],
},
{
date: "2026-09-04",
tag: "匹配资料 · 私信头",

View File

@ -2767,6 +2767,22 @@ img { max-width: 100%; display: block; }
margin-bottom: 48px;
}
.profile-connect-block {
margin-bottom: 48px;
}
.profile-connect-title {
font-size: 0.95rem;
font-weight: 600;
margin: 0 0 12px;
color: var(--text-muted);
letter-spacing: 0.02em;
}
.profile-connect-stats {
margin-bottom: 0;
}
.profile-stat-card {
background: var(--bg-card);
border: var(--border-glass);
@ -2776,6 +2792,18 @@ img { max-width: 100%; display: block; }
backdrop-filter: blur(20px);
}
a.profile-stat-link {
display: block;
color: inherit;
text-decoration: none;
transition: border-color 0.2s ease, transform 0.2s ease;
}
a.profile-stat-link:hover {
border-color: var(--accent, #e8a87c);
transform: translateY(-2px);
}
.profile-stat-card strong {
display: block;
font-size: 1.75rem;

View File

@ -60,6 +60,7 @@ export default function ProfilePage() {
const [stats, setStats] = useState<ProfileStats | null>(null);
const [favoriteDests, setFavoriteDests] = useState<Destination[]>([]);
const [rsvps, setRsvps] = useState<Meetup[]>([]);
const [connect, setConnect] = useState({ likes: 0, matches: 0, chats: 0 });
const [trip, setTrip] = useState<TripItem[]>([]);
const [meta, setMeta] = useState<PlanMeta | null>(null);
const [loading, setLoading] = useState(true);
@ -109,11 +110,21 @@ export default function ProfilePage() {
const idSet = new Set(mine.ids || []);
return all.filter((m) => idSet.has(m.id)).slice(0, 6);
}),
]).then(([s, f, v, myMeetups]) => {
Promise.all([
api.getMatchLikesReceived(token).catch(() => []),
api.getMutualMatches(token).catch(() => []),
api.getConversations(token).catch(() => []),
]).then(([likes, matches, chats]) => ({
likes: likes.length,
matches: matches.length,
chats: chats.length,
})),
]).then(([s, f, v, myMeetups, connectStats]) => {
setStats(s);
setFavoriteDests(f);
setVip(Boolean(v.vip));
setRsvps(myMeetups);
setConnect(connectStats);
}).catch(() => {}).finally(() => setLoading(false));
refreshPlan();
@ -220,6 +231,32 @@ export default function ProfilePage() {
</div>
)}
<div className="profile-connect-block">
<h2 className="profile-connect-title">{t.profile.connectStrip}</h2>
<div className="profile-stats-grid profile-connect-stats">
<Link href="/dating/likes" className="profile-stat-card profile-stat-link">
<span className="stat-emoji">💕</span>
<strong>{connect.likes}</strong>
<span>{t.profile.statLikes}</span>
</Link>
<Link href="/dating" className="profile-stat-card profile-stat-link">
<span className="stat-emoji">✨</span>
<strong>{connect.matches}</strong>
<span>{t.profile.statMatches}</span>
</Link>
<Link href="/chat" className="profile-stat-card profile-stat-link">
<span className="stat-emoji">✉️</span>
<strong>{connect.chats}</strong>
<span>{t.profile.statChats}</span>
</Link>
<Link href="/meetups" className="profile-stat-card profile-stat-link">
<span className="stat-emoji">🎉</span>
<strong>{rsvps.length}</strong>
<span>{t.profile.statRsvps}</span>
</Link>
</div>
</div>
<NomadDigest rsvpCount={rsvps.length} upcoming={rsvps} />
<section className="profile-trip">

View File

@ -128,9 +128,10 @@ export default function MeetupsClient({ meetups }: Props) {
try {
const res = await api.rsvpMeetup(meetup.id, undefined, token);
setRsvpIds(new Set(rsvpIds).add(meetup.id));
const live = meetup.mode === "online" || meetup.mode === "hybrid";
toast(res.message || t.meetups.rsvpOk, "success", {
href: "/dating",
label: t.meetups.goDating,
href: live ? meetupEventHref(meetup) : "/profile",
label: live ? t.meetups.liveBtn : t.meetups.rsvpCalCta,
});
void loadPeers(meetup.id);
} catch {

View File

@ -685,6 +685,7 @@ export const zh = {
tripEvents: "行程城市活动",
meetPeers: "同城可能认识的人",
goDating: "去匹配",
rsvpCalCta: "导出日历",
viewProfile: "查看资料",
},
community: {
@ -1074,6 +1075,11 @@ export const zh = {
statCities: "计划城市",
statReady: "出发就绪",
statBadges: "成就徽章",
connectStrip: "Connect 动态",
statLikes: "喜欢我的",
statMatches: "互相喜欢",
statChats: "私信会话",
statRsvps: "已报名活动",
openPlan: "打开计划中心",
tripEmpty: "还没有规划行程",
goPlan: "去规划 →",
@ -1953,6 +1959,7 @@ export const en: { [K in keyof typeof zh]: typeof zh[K] extends string ? string
tripEvents: "Events near your trip",
meetPeers: "People you may meet nearby",
goDating: "Go match",
rsvpCalCta: "Export calendar",
viewProfile: "View profile",
},
community: {
@ -2294,6 +2301,11 @@ export const en: { [K in keyof typeof zh]: typeof zh[K] extends string ? string
statCities: "Plan cities",
statReady: "Readiness",
statBadges: "Badges",
connectStrip: "Connect pulse",
statLikes: "Liked you",
statMatches: "Matches",
statChats: "Chats",
statRsvps: "RSVPs",
openPlan: "Open plan hub",
tripEmpty: "No trip planned yet",
goPlan: "Start planning →",