From 06ab949d810b3cf5f661e4d70d63ce39e20ad9e3 Mon Sep 17 00:00:00 2001 From: eric Date: Mon, 8 Jun 2026 06:05:31 -0500 Subject: [PATCH] 's' --- .gitignore | 1 + app/[locale]/dating/components/LikesPanel.tsx | 39 +- app/[locale]/dating/components/SwipeCard.tsx | 48 +- app/[locale]/dating/components/SwipeStack.tsx | 6 + app/[locale]/dating/page.tsx | 92 ++- app/[locale]/dating/utils.ts | 16 +- app/[locale]/discuss/[id]/page.tsx | 268 +++++++ app/[locale]/discuss/page.tsx | 61 +- app/[locale]/join/page.tsx | 53 +- app/[locale]/layout.tsx | 9 +- app/[locale]/meetups/host/page.tsx | 20 +- app/[locale]/meetups/page.tsx | 194 ++++- app/[locale]/members/[id]/page.tsx | 141 ++++ app/[locale]/page.tsx | 31 +- app/components/ChatNavLink.tsx | 63 ++ app/components/CityCard.tsx | 48 +- app/components/CityModal.tsx | 9 +- app/components/DevDebugFab.tsx | 154 ++++ app/components/DevDebugTools.tsx | 14 + app/components/Footer.tsx | 11 +- app/components/HeroSection.tsx | 37 +- app/components/Navbar.tsx | 163 +++-- app/components/SocialGreetButton.tsx | 49 ++ app/components/UserAvatar.tsx | 13 +- app/components/meetups/EventLiveRoom.tsx | 7 + app/components/meetups/MeetupSocialPanel.tsx | 95 +++ app/context/DevDebugContext.tsx | 122 ++++ app/data/cities-extra.ts | 677 ++++++++++++++++++ app/data/cities.ts | 3 + app/data/map-cities.ts | 28 + app/lib/chat.ts | 13 + app/lib/city-slugs.ts | 28 + app/lib/city-weather.ts | 64 ++ app/lib/meetups.ts | 2 +- app/lib/payment/usePayStatusPoll.ts | 1 - app/lib/social.ts | 97 +++ backend/__pycache__/init_db.cpython-312.pyc | Bin 19960 -> 20345 bytes backend/__pycache__/main.cpython-312.pyc | Bin 150098 -> 181306 bytes backend/__pycache__/pb_client.cpython-312.pyc | Bin 6942 -> 7234 bytes backend/__pycache__/seed_data.cpython-312.pyc | Bin 36631 -> 36725 bytes backend/cities_extra.py | 32 + backend/dm.py | 29 + backend/init_db.py | 19 +- backend/main.py | 654 +++++++++++++++-- backend/meetup_live.py | 2 +- backend/payment.py | 9 +- backend/pb_client.py | 7 + backend/seed_data.py | 5 +- backend/settings.py | 3 +- backend/weather.py | 121 ++++ config/debug-features.ts | 137 ++++ config/home.config.ts | 8 +- config/index.ts | 12 +- config/nav.config.ts | 26 +- cursor.env.example | 37 + messages/en.json | 4 +- messages/zh.json | 2 + scripts/dev-tunnel.py | 23 +- scripts/load_dev_env.py | 54 ++ scripts/ssh-build-frontend-prod.py | 8 +- scripts/ssh-check-docker-mirror.py | 8 +- scripts/ssh-check-ntfy.py | 8 +- scripts/ssh-cleanup-heavy.py | 8 +- scripts/ssh-fix-deploy.py | 8 +- scripts/ssh-fix-lounge.py | 8 +- scripts/ssh-fix-ntfy.py | 8 +- scripts/ssh-git-pull-prod.py | 8 +- scripts/ssh-ntfy-auth.py | 8 +- scripts/ssh-patch-join-page.py | 8 +- scripts/ssh-probe-server.py | 7 +- scripts/ssh-restart-listmonk.py | 8 +- scripts/ssh-restore-lounge.py | 8 +- scripts/ssh-setup-ntfy-acl.py | 8 +- scripts/ssh-setup-ntfy-acl2.py | 8 +- scripts/ssh-setup-ntfy-acl3.py | 8 +- scripts/ssh-sync-ntfy-backend.py | 8 +- scripts/ssh-verify-prod.py | 8 +- 77 files changed, 3646 insertions(+), 358 deletions(-) create mode 100644 app/[locale]/discuss/[id]/page.tsx create mode 100644 app/[locale]/members/[id]/page.tsx create mode 100644 app/components/ChatNavLink.tsx create mode 100644 app/components/DevDebugFab.tsx create mode 100644 app/components/DevDebugTools.tsx create mode 100644 app/components/SocialGreetButton.tsx create mode 100644 app/components/meetups/MeetupSocialPanel.tsx create mode 100644 app/context/DevDebugContext.tsx create mode 100644 app/data/cities-extra.ts create mode 100644 app/lib/city-weather.ts create mode 100644 app/lib/social.ts create mode 100644 backend/cities_extra.py create mode 100644 backend/weather.py create mode 100644 config/debug-features.ts create mode 100644 cursor.env.example create mode 100644 scripts/load_dev_env.py diff --git a/.gitignore b/.gitignore index 2f23f3c..bd44de7 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ yarn-error.log* # env files (can opt-in for committing if needed) # .env* +cursor.env # vercel .vercel diff --git a/app/[locale]/dating/components/LikesPanel.tsx b/app/[locale]/dating/components/LikesPanel.tsx index cd8d758..3203073 100644 --- a/app/[locale]/dating/components/LikesPanel.tsx +++ b/app/[locale]/dating/components/LikesPanel.tsx @@ -1,5 +1,6 @@ "use client"; +import type { ReactNode } from "react"; import { Link } from "@/i18n/navigation"; import { mediaUrl } from "@/app/lib/media"; import type { DatingProfile } from "../types"; @@ -10,6 +11,8 @@ interface LikesPanelProps { formatTime: (value?: string) => string; t: (key: string) => string; onSelectProfile?: (profile: DatingProfile) => void; + onLikeBack?: (profile: DatingProfile) => void; + likingBackId?: string | null; } function ProfileRow({ @@ -17,11 +20,13 @@ function ProfileRow({ timeLabel, badge, onSelect, + action, }: { profile: DatingProfile; timeLabel: string; badge?: string; onSelect?: () => void; + action?: ReactNode; }) { const className = "flex w-full items-center gap-3 rounded-xl border border-gray-100 bg-white p-3 text-left shadow-sm transition-shadow hover:shadow-md dark:border-gray-800 dark:bg-gray-900"; @@ -41,9 +46,12 @@ function ProfileRow({ ) : null} - - {timeLabel} - +
+ {action} + + {timeLabel} + +
); if (onSelect) { @@ -56,7 +64,15 @@ function ProfileRow({ return
{content}
; } -export default function LikesPanel({ likes, mutual, formatTime, t, onSelectProfile }: LikesPanelProps) { +export default function LikesPanel({ + likes, + mutual, + formatTime, + t, + onSelectProfile, + onLikeBack, + likingBackId, +}: LikesPanelProps) { return (
@@ -77,6 +93,21 @@ export default function LikesPanel({ likes, mutual, formatTime, t, onSelectProfi profile={profile} timeLabel={formatTime(profile.likedAt)} onSelect={() => onSelectProfile?.(profile)} + action={ + onLikeBack ? ( + + ) : null + } /> )) ) : ( diff --git a/app/[locale]/dating/components/SwipeCard.tsx b/app/[locale]/dating/components/SwipeCard.tsx index 66c7fea..486330b 100644 --- a/app/[locale]/dating/components/SwipeCard.tsx +++ b/app/[locale]/dating/components/SwipeCard.tsx @@ -12,6 +12,8 @@ interface SwipeCardProps { isAnimatingOut: "left" | "right" | null; onLike: () => void; onDislike: () => void; + onSuperLike?: () => void; + showSuperLike?: boolean; onBeginDrag: (clientX: number) => void; onMoveDrag: (clientX: number) => void; onEndDrag: () => void; @@ -26,6 +28,8 @@ export default function SwipeCard({ isAnimatingOut, onLike, onDislike, + onSuperLike, + showSuperLike = false, onBeginDrag, onMoveDrag, onEndDrag, @@ -76,17 +80,20 @@ export default function SwipeCard({ e.stopPropagation()} > {t("report")}
- {profile.name} + e.stopPropagation()}> + {profile.name} +
@@ -114,11 +121,17 @@ export default function SwipeCard({

{profile.bio}

-
+
e.stopPropagation()} + >
{t("dislike")}
+ {showSuperLike && onSuperLike ? ( +
+ + {t("superlike")} +
+ ) : null}
diff --git a/app/[locale]/dating/utils.ts b/app/[locale]/dating/utils.ts index 3a6e83f..ac4d438 100644 --- a/app/[locale]/dating/utils.ts +++ b/app/[locale]/dating/utils.ts @@ -45,10 +45,20 @@ export function filterFallbackProfiles( ) { return false; } - if (advanced?.gender && advanced.gender !== "all" && profile.gender !== advanced.gender) { + if ( + advanced?.gender && + advanced.gender !== "all" && + advanced.gender !== "不限" && + profile.gender !== advanced.gender + ) { return false; } - if (advanced?.single && advanced.single !== "all" && profile.single !== advanced.single) { + if ( + advanced?.single && + advanced.single !== "all" && + advanced.single !== "不限" && + profile.single !== advanced.single + ) { return false; } return true; @@ -123,7 +133,7 @@ export async function fetchMutualMatches(): Promise { export async function saveSwipe( profileId: string, - action: "like" | "dislike", + action: "like" | "dislike" | "superlike", intent: MatchIntent ): Promise { return apiFetch("/api/matches/swipes", { diff --git a/app/[locale]/discuss/[id]/page.tsx b/app/[locale]/discuss/[id]/page.tsx new file mode 100644 index 0000000..48c5e6f --- /dev/null +++ b/app/[locale]/discuss/[id]/page.tsx @@ -0,0 +1,268 @@ +"use client"; + +import { FormEvent, useEffect, useState } from "react"; +import { Link, useLocale } from "@/i18n/navigation"; +import { useParams } from "next/navigation"; +import Footer from "@/app/components/Footer"; +import { fetchAuthMe } from "@/app/lib/api-client"; +import { + fetchDiscussion, + fetchDiscussionReplies, + postDiscussionReply, + toggleDiscussionLike, + toggleDiscussionPin, + type DiscussionReply, +} from "@/app/lib/social"; + +export default function DiscussThreadPage() { + const params = useParams(); + const discussionId = String(params?.id || ""); + const locale = useLocale(); + const [topic, setTopic] = useState | null>(null); + const [replies, setReplies] = useState([]); + const [draft, setDraft] = useState(""); + const [loading, setLoading] = useState(true); + const [posting, setPosting] = useState(false); + const [isLoggedIn, setIsLoggedIn] = useState(false); + const [currentUserId, setCurrentUserId] = useState(""); + const [liking, setLiking] = useState(false); + const [pinning, setPinning] = useState(false); + const [error, setError] = useState(""); + + useEffect(() => { + fetchAuthMe().then((data) => { + const userId = String(data?.user?.id || ""); + setIsLoggedIn(Boolean(userId)); + setCurrentUserId(userId); + }); + }, []); + + useEffect(() => { + if (!discussionId) return; + let cancelled = false; + Promise.all([fetchDiscussion(discussionId), fetchDiscussionReplies(discussionId)]) + .then(([topicData, replyItems]) => { + if (cancelled) return; + setTopic(topicData.item); + setReplies(replyItems); + }) + .catch(() => { + if (!cancelled) setError(locale === "zh" ? "加载失败" : "Failed to load"); + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + return () => { + cancelled = true; + }; + }, [discussionId, locale]); + + const handleLike = async () => { + if (!isLoggedIn || !discussionId) return; + setLiking(true); + setError(""); + try { + const result = await toggleDiscussionLike(discussionId); + setTopic((current) => + current + ? { + ...current, + likes: result.likes, + likedByMe: result.liked, + } + : current + ); + } catch (err) { + setError(err instanceof Error ? err.message : locale === "zh" ? "点赞失败" : "Like failed"); + } finally { + setLiking(false); + } + }; + + const handlePin = async () => { + if (!isLoggedIn || !discussionId) return; + setPinning(true); + setError(""); + try { + const result = await toggleDiscussionPin(discussionId); + setTopic((current) => + current + ? { + ...current, + pinned: result.pinned, + } + : current + ); + } catch (err) { + setError(err instanceof Error ? err.message : locale === "zh" ? "置顶失败" : "Pin failed"); + } finally { + setPinning(false); + } + }; + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + const body = draft.trim(); + if (!body || !discussionId) return; + if (!isLoggedIn) { + setError(locale === "zh" ? "请先登录后再回复" : "Please log in to reply"); + return; + } + setPosting(true); + setError(""); + try { + const reply = await postDiscussionReply(discussionId, body); + setReplies((current) => [...current, reply]); + setDraft(""); + setTopic((current) => + current ? { ...current, replies: Number(current.replies || 0) + 1 } : current + ); + } catch (err) { + setError(err instanceof Error ? err.message : locale === "zh" ? "回复失败" : "Reply failed"); + } finally { + setPosting(false); + } + }; + + if (loading) { + return ( +
+
+
+
+
+ ); + } + + if (!topic) { + return ( +
+
+

{error || (locale === "zh" ? "话题不存在" : "Topic not found")}

+ + {locale === "zh" ? "返回讨论区" : "Back to discussions"} + +
+
+ ); + } + + const tags = Array.isArray(topic.tags) ? topic.tags.map(String) : []; + const likedByMe = Boolean(topic.likedByMe); + const likes = Number(topic.likes || 0); + const pinned = Boolean(topic.pinned); + const isAuthor = Boolean(currentUserId && String(topic.authorUserId || "") === currentUserId); + + return ( +
+
+ + ← {locale === "zh" ? "返回讨论区" : "Back"} + + +
+
+ {tags.map((tag) => ( + + {tag} + + ))} +
+
+

{String(topic.title || "")}

+ {pinned ? ( + + 📌 {locale === "zh" ? "置顶" : "Pinned"} + + ) : null} +
+

+ {String(topic.author || "社区成员")} + {topic.city ? ` · ${String(topic.city)}` : ""} + {` · 👁 ${Number(topic.views || 0)} · 💬 ${Number(topic.replies || 0)}`} +

+
+ + {isAuthor ? ( + + ) : null} +
+ {topic.excerpt ? ( +

+ {String(topic.excerpt)} +

+ ) : null} +
+ +
+

+ {locale === "zh" ? "回复" : "Replies"} ({replies.length}) +

+ {replies.length ? ( + replies.map((reply) => ( +
+
+ + {reply.author || (locale === "zh" ? "成员" : "Member")} + + {reply.created ? new Date(reply.created).toLocaleString() : ""} +
+

{reply.body}

+
+ )) + ) : ( +

+ {locale === "zh" ? "还没有回复,来抢沙发吧" : "No replies yet"} +

+ )} +
+ +
+