diff --git a/backend/app/data/community_data.py b/backend/app/data/community_data.py index e6f96b1..9e23cd0 100644 --- a/backend/app/data/community_data.py +++ b/backend/app/data/community_data.py @@ -619,6 +619,72 @@ DISCUSSION_REPLIES = { "like_count": 15, }, ], + "health-insurance": [ + { + "id": "r1", + "author": "Sofia", + "author_emoji": "🏥", + "content": "30 岁以下 SafetyWing 够用;有慢性病建议加本地门诊险。", + "created_at": "2026-08-28", + "like_count": 10, + }, + { + "id": "r2", + "author": "Ken", + "author_emoji": "🇯🇵", + "content": "在日本长期停留我叠了本地国民健康保险,报销更稳。", + "created_at": "2026-08-28", + "like_count": 7, + }, + ], + "meetup-feedback": [ + { + "id": "r1", + "author": "Lina", + "author_emoji": "📷", + "content": "大理 + 清迈希望能固定每月一场创作局。", + "created_at": "2026-08-29", + "like_count": 18, + }, + { + "id": "r2", + "author": "Omar", + "author_emoji": "🏙️", + "content": "迪拜适合枢纽月见面,建议放周末早午餐。", + "created_at": "2026-08-29", + "like_count": 9, + }, + ], + "berlin-winter": [ + { + "id": "r1", + "author": "Devon", + "author_emoji": "🖥️", + "content": "冬天把联合办公当社交主场,再加一周一次室内攀岩/跑步局。", + "created_at": "2026-09-02", + "like_count": 11, + }, + ], + "dubai-summer": [ + { + "id": "r1", + "author": "Nina", + "author_emoji": "🍷", + "content": "夏天基本只在空调空间办公,预算里把交通和水电预留高一点。", + "created_at": "2026-09-04", + "like_count": 6, + }, + ], + "seoul-visa": [ + { + "id": "r1", + "author": "Jin", + "author_emoji": "🇰🇷", + "content": "最近材料重点是收入证明和保险;审批大约 3–6 周,建议提前办。", + "created_at": "2026-09-05", + "like_count": 8, + }, + ], } NOMAD_ROUTES = [ diff --git a/backend/app/services/community_store.py b/backend/app/services/community_store.py index 755a4cc..22a4edd 100644 --- a/backend/app/services/community_store.py +++ b/backend/app/services/community_store.py @@ -568,6 +568,9 @@ _JSON_PATH = Path(__file__).resolve().parents[1] / "data" / "community_store.jso _j: dict[str, Any] = {} +_SEED_VERSION = 3 + + def _jload() -> None: global _j if _j: @@ -580,19 +583,27 @@ def _jload() -> None: needs_seed = not _j.get("seeded") or not ( (_j.get("meetups") or []) and (_j.get("discussions") or []) and (_j.get("gigs") or []) ) - if needs_seed: + version_bump = int(_j.get("seed_version") or 0) < _SEED_VERSION + if needs_seed or version_bump: + keep_notif = _j.get("notifications") or {} + keep_rsvp = _j.get("rsvps") or {} + keep_apps = _j.get("gig_apps") or [] + keep_feedback = _j.get("feedback") or [] + keep_views = _j.get("views") or {} + keep_likes = _j.get("discussion_likes") or {} _j.update({ "meetups": deepcopy(community_data.MEETUPS), "discussions": deepcopy(community_data.DISCUSSIONS), "replies": deepcopy(community_data.DISCUSSION_REPLIES), "gigs": deepcopy(getattr(community_data, "GIGS", [])), - "discussion_likes": _j.get("discussion_likes") or {}, - "rsvps": _j.get("rsvps") or {}, - "gig_apps": _j.get("gig_apps") or [], - "notifications": _j.get("notifications") or {}, - "feedback": _j.get("feedback") or [], - "views": _j.get("views") or {}, + "discussion_likes": keep_likes, + "rsvps": keep_rsvp, + "gig_apps": keep_apps, + "notifications": keep_notif, + "feedback": keep_feedback, + "views": keep_views, "seeded": True, + "seed_version": _SEED_VERSION, }) _jsave() diff --git a/backend/app/services/social_store.py b/backend/app/services/social_store.py index c7efc4b..bac8f56 100644 --- a/backend/app/services/social_store.py +++ b/backend/app/services/social_store.py @@ -140,7 +140,7 @@ def get_or_create_profile(user_id: str, name: str, **extra: Any) -> dict: def join_member(user_id: str, name: str, payload: dict) -> dict: - return get_or_create_profile( + profile = get_or_create_profile( user_id, name, location=payload.get("city", "全球"), @@ -151,6 +151,11 @@ def join_member(user_id: str, name: str, payload: dict) -> dict: lookingFor=payload.get("lookingFor", ["friends", "explore"]), photo=payload.get("photo", "🧑‍💻"), ) + try: + seed_demo_social_activity(user_id) + except Exception: + pass + return profile def get_public_profile(user_id: str) -> dict | None: @@ -670,6 +675,178 @@ def mark_order_paid(order_id: str) -> dict | None: return order +def seed_demo_social_activity(user_id: str) -> None: + """Give a real user a few incoming likes + one chat so Connect feels alive.""" + if not user_id: + return + my_pid = f"user-{user_id}" + peers = [p for p in CANDIDATE_PROFILES if p.get("userId")][:4] + if not peers: + return + + buddy = peers[0] + buddy_uid = buddy["userId"] + already = False + if use_pb(): + already = bool(safe_first("swipes", filter=f"userId={q(buddy_uid)} && profileId={q(my_pid)}")) + else: + _jload() + already = bool((_j.get("demo_seeded") or {}).get(user_id)) or any( + s.get("userId") == buddy_uid and s.get("profileId") == my_pid for s in (_j.get("swipes") or []) + ) + if already: + return + + # Incoming likes from candidates toward this user + for p in peers[:3]: + peer_uid = p["userId"] + if use_pb(): + if not safe_first("swipes", filter=f"userId={q(peer_uid)} && profileId={q(my_pid)}"): + safe_create( + "swipes", + { + "userId": peer_uid, + "profileId": my_pid, + "action": "like", + "intent": "friends", + "swipeDate": _today_key(), + }, + ) + else: + _jload() + swipes = _j.setdefault("swipes", []) + if not any(s.get("userId") == peer_uid and s.get("profileId") == my_pid for s in swipes): + swipes.append( + { + "id": secrets.token_hex(6), + "userId": peer_uid, + "profileId": my_pid, + "action": "like", + "intent": "friends", + "date": _today_key(), + "createdAt": _now_iso(), + } + ) + + # Mutual match + messages with first peer + buddy_pid = buddy["id"] + if use_pb(): + if not safe_first("swipes", filter=f"userId={q(user_id)} && profileId={q(buddy_pid)}"): + safe_create( + "swipes", + { + "userId": user_id, + "profileId": buddy_pid, + "action": "like", + "intent": "friends", + "swipeDate": _today_key(), + }, + ) + else: + _jload() + swipes = _j.setdefault("swipes", []) + if not any(s.get("userId") == user_id and s.get("profileId") == buddy_pid for s in swipes): + swipes.append( + { + "id": secrets.token_hex(6), + "userId": user_id, + "profileId": buddy_pid, + "action": "like", + "intent": "friends", + "date": _today_key(), + "createdAt": _now_iso(), + } + ) + + match = _ensure_match(user_id, buddy, "friends") + conv_id = (match or {}).get("conversationId") or "" + if not conv_id: + conv = _ensure_conversation(user_id, buddy_uid, intent="friends", match=True) + conv_id = conv.get("id", "") + + hello = f"嗨!我在{buddy.get('location', '旅途中')},看到你也在用 nomadro,交个朋友呀 👋" + reply = "你好!我也刚到,有机会一起去联合办公或 meetup~" + + if conv_id: + if use_pb(): + msgs = safe_list("messages", filter=f"conversationId={q(conv_id)}") + if not msgs: + safe_create("messages", {"conversationId": conv_id, "senderId": buddy_uid, "body": hello}) + safe_create("messages", {"conversationId": conv_id, "senderId": user_id, "body": reply}) + safe_update( + "conversations", + conv_id, + { + "lastMessageAt": _now_iso(), + "lastMessagePreview": reply[:80], + "readState": {user_id: _now_iso(), buddy_uid: _now_iso()}, + }, + ) + else: + _jload() + msgs = [m for m in (_j.get("messages") or []) if m.get("conversationId") == conv_id] + if not msgs: + _j.setdefault("messages", []).extend( + [ + { + "id": secrets.token_hex(8), + "conversationId": conv_id, + "senderId": buddy_uid, + "body": hello, + "createdAt": _now_iso(), + }, + { + "id": secrets.token_hex(8), + "conversationId": conv_id, + "senderId": user_id, + "body": reply, + "createdAt": _now_iso(), + }, + ] + ) + for c in _j.get("conversations") or []: + if c.get("id") == conv_id: + c["lastMessageAt"] = _now_iso() + c["lastMessagePreview"] = reply[:80] + break + + try: + from app.services import community_store + from app.services.notify import notify_user + + notify_user( + user_id, + "有人喜欢了你", + f"{buddy.get('name', '游民')} 等人对你感兴趣,去看看吧", + "/dating/likes", + "match", + ) + if conv_id: + notify_user( + user_id, + "新消息", + f"{buddy.get('name', '游民')}: {hello[:40]}", + f"/chat/{conv_id}", + "chat", + ) + community_store.add_notification( + user_id, + "欢迎来到匹配", + "左滑跳过、右滑喜欢、上滑超级喜欢。匹配成功可私信。", + "/dating", + "system", + ) + except Exception: + pass + + if not use_pb(): + _jload() + _j.setdefault("demo_seeded", {})[user_id] = True + _jsave() + else: + pass + + # ── JSON fallback ────────────────────────────────────────────────────── def _jload() -> dict: diff --git a/frontend/src/app/changelog/page.tsx b/frontend/src/app/changelog/page.tsx index 255636a..f94a1ad 100644 --- a/frontend/src/app/changelog/page.tsx +++ b/frontend/src/app/changelog/page.tsx @@ -8,6 +8,14 @@ export const metadata: Metadata = { }; const LOGS = [ + { + date: "2026-09-04", + tag: "匹配滑动 · 喜欢我的", + items: [ + "匹配卡片支持左右/上滑手势与方向键;喜欢页区分「喜欢我的 / 我喜欢的」", + "加入匹配时自动注入演示私信与来赞,讨论回复补全", + ], + }, { date: "2026-09-04", tag: "匹配页修复", diff --git a/frontend/src/components/DatingLikesClient.tsx b/frontend/src/components/DatingLikesClient.tsx index f1b3903..3db98b9 100644 --- a/frontend/src/components/DatingLikesClient.tsx +++ b/frontend/src/components/DatingLikesClient.tsx @@ -9,11 +9,15 @@ import type { MatchProfile } from "@/lib/types"; import RingNext from "@/components/RingNext"; import { useRingSteps } from "@/lib/rings"; +type Tab = "liked" | "received"; + export default function DatingLikesClient() { const { user, token } = useAuth(); const { t } = useI18n(); const rings = useRingSteps(); - const [likes, setLikes] = useState([]); + const [tab, setTab] = useState("received"); + const [liked, setLiked] = useState([]); + const [received, setReceived] = useState([]); const [loading, setLoading] = useState(Boolean(token)); useEffect(() => { @@ -22,13 +26,20 @@ export default function DatingLikesClient() { return; } setLoading(true); - api - .getMatchLikes(token) - .then(setLikes) - .catch(() => setLikes([])) + Promise.all([ + api.getMatchLikes(token).catch(() => [] as MatchProfile[]), + api.getMatchLikesReceived(token).catch(() => [] as MatchProfile[]), + ]) + .then(([mine, incoming]) => { + setLiked(mine); + setReceived(incoming); + if (incoming.length === 0 && mine.length > 0) setTab("liked"); + }) .finally(() => setLoading(false)); }, [token]); + const list = tab === "liked" ? liked : received; + if (!user) { return (
@@ -49,16 +60,40 @@ export default function DatingLikesClient() {
{t.dating.tag}

{t.dating.likesTitle}

-

{t.dating.likesSubtitle}

+

{tab === "liked" ? t.dating.likesSubtitle : t.dating.likesReceivedSubtitle}

+ +
+ + +
+ {loading &&

{t.common.loading}

} - {!loading && likes.length === 0 && ( + {!loading && list.length === 0 && (
-

{t.dating.likesEmpty}

+

+ {tab === "liked" ? t.dating.likesEmpty : t.dating.likesReceivedEmpty} +

{t.dating.goSwipe} @@ -71,11 +106,11 @@ export default function DatingLikesClient() { )}
{!loading && - likes.map((p) => { + list.map((p) => { const photo = p.photo || ""; const href = p.userId ? `/members/${p.userId}` : "/dating"; return ( - +
{photo.startsWith("http") ? ( // eslint-disable-next-line @next/next/no-img-element @@ -86,6 +121,9 @@ export default function DatingLikesClient() {
{p.name}

{p.location || t.dating.globalNomad}

+ {tab === "received" && ( + {t.dating.likesYou} + )} ); })} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 7e1ce2b..fedd719 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -351,6 +351,10 @@ export const api = { getMemberProfile: (userId: string) => fetchAPI(`/members/${userId}`), getMatchLikes: (token: string) => fetchAPI("/social/matches/likes", { headers: { Authorization: `Bearer ${token}` } }), + getMatchLikesReceived: (token: string) => + fetchAPI("/social/matches/likes/received", { + headers: { Authorization: `Bearer ${token}` }, + }), undoSwipe: (token: string) => fetchAPI<{ ok: boolean }>("/social/matches/swipes/undo", { method: "POST", diff --git a/frontend/src/lib/i18n/dictionaries.ts b/frontend/src/lib/i18n/dictionaries.ts index 7868c18..34b5542 100644 --- a/frontend/src/lib/i18n/dictionaries.ts +++ b/frontend/src/lib/i18n/dictionaries.ts @@ -452,6 +452,11 @@ export const zh = { swipeLeft: "跳过", swipeRight: "喜欢", swipeUp: "超级喜欢", + likesReceivedTab: "喜欢我的", + likesMineTab: "我喜欢的", + likesReceivedSubtitle: "对你右滑的游民会出现在这里", + likesReceivedEmpty: "还没有人喜欢你,先去完善资料并滑动匹配", + likesYou: "喜欢了你", }, chat: { tag: "✉️ MESSAGES", @@ -1712,6 +1717,11 @@ export const en: { [K in keyof typeof zh]: typeof zh[K] extends string ? string swipeLeft: "Pass", swipeRight: "Like", swipeUp: "Super like", + likesReceivedTab: "Liked you", + likesMineTab: "You liked", + likesReceivedSubtitle: "People who swiped right on you", + likesReceivedEmpty: "No incoming likes yet — keep your profile fresh and swipe", + likesYou: "Liked you", }, chat: { tag: "✉️ MESSAGES",