diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png index 0c1d427..4e4e6ea 100644 Binary files a/public/apple-touch-icon.png and b/public/apple-touch-icon.png differ diff --git a/public/blindbox-hero.png b/public/blindbox-hero.png new file mode 100644 index 0000000..39dfa1b Binary files /dev/null and b/public/blindbox-hero.png differ diff --git a/public/empty-no-record.png b/public/empty-no-record.png new file mode 100644 index 0000000..cceceee Binary files /dev/null and b/public/empty-no-record.png differ diff --git a/public/empty-no-room.png b/public/empty-no-room.png new file mode 100644 index 0000000..559ce26 Binary files /dev/null and b/public/empty-no-room.png differ diff --git a/public/error-robot.png b/public/error-robot.png new file mode 100644 index 0000000..b2f4594 Binary files /dev/null and b/public/error-robot.png differ diff --git a/public/icon-192x192.png b/public/icon-192x192.png index 3fcc54e..29c3766 100644 Binary files a/public/icon-192x192.png and b/public/icon-192x192.png differ diff --git a/public/icon-512x512.png b/public/icon-512x512.png index 281918f..bb76514 100644 Binary files a/public/icon-512x512.png and b/public/icon-512x512.png differ diff --git a/public/og-image.png b/public/og-image.png new file mode 100644 index 0000000..86ac8ae Binary files /dev/null and b/public/og-image.png differ diff --git a/public/panic-hero.png b/public/panic-hero.png new file mode 100644 index 0000000..4adda05 Binary files /dev/null and b/public/panic-hero.png differ diff --git a/public/restaurant-fallback.png b/public/restaurant-fallback.png new file mode 100644 index 0000000..f1d0627 Binary files /dev/null and b/public/restaurant-fallback.png differ diff --git a/public/share-bg-blindbox.png b/public/share-bg-blindbox.png new file mode 100644 index 0000000..214d896 Binary files /dev/null and b/public/share-bg-blindbox.png differ diff --git a/public/share-bg-panic.png b/public/share-bg-panic.png new file mode 100644 index 0000000..1764a50 Binary files /dev/null and b/public/share-bg-panic.png differ diff --git a/src/app/achievements/page.tsx b/src/app/achievements/page.tsx index 644c174..a603997 100644 --- a/src/app/achievements/page.tsx +++ b/src/app/achievements/page.tsx @@ -193,6 +193,7 @@ export default function AchievementsPage() { ) : decisions.length === 0 ? ( + + 平日蓄水,周末开奖。把所有"想做但一直没做"的事,交给命运来决定。 diff --git a/src/app/error.tsx b/src/app/error.tsx index 0c34e22..4293042 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -2,7 +2,7 @@ import { useEffect } from "react"; import { motion } from "framer-motion"; -import { AlertTriangle, RotateCcw, Home } from "lucide-react"; +import { RotateCcw, Home } from "lucide-react"; import Button from "@/components/Button"; export default function Error({ @@ -23,14 +23,13 @@ export default function Error({ initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} > - -
- - + />

出了点问题

diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx index 6cfba47..0030977 100644 --- a/src/app/global-error.tsx +++ b/src/app/global-error.tsx @@ -17,7 +17,7 @@ export default function GlobalError({

-
⚠️
+ 错误

应用崩溃了

发生了严重错误,请尝试刷新页面 diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e622b2d..5277fb4 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -12,9 +12,21 @@ const geistSans = Geist({ }); export const metadata: Metadata = { + metadataBase: new URL(process.env.NEXT_PUBLIC_BASE_URL || "https://nowhatever.app"), title: "NoWhatever — 别说随便", description: "像 Tinder 一样滑卡片,和朋友一起决定去哪吃!", referrer: "no-referrer", + openGraph: { + title: "NoWhatever — 别说随便", + description: "像 Tinder 一样滑卡片,和朋友一起决定去哪吃!", + images: [{ url: "/og-image.png", width: 1200, height: 630 }], + }, + twitter: { + card: "summary_large_image", + title: "NoWhatever — 别说随便", + description: "像 Tinder 一样滑卡片,和朋友一起决定去哪吃!", + images: ["/og-image.png"], + }, }; export const viewport: Viewport = { diff --git a/src/app/panic/page.tsx b/src/app/panic/page.tsx index a3ea0e8..7cbd9cf 100644 --- a/src/app/panic/page.tsx +++ b/src/app/panic/page.tsx @@ -214,11 +214,20 @@ export default function PanicPage() {

+ + {sceneConfig.subtitle} diff --git a/src/app/room/[id]/page.tsx b/src/app/room/[id]/page.tsx index e7c25c2..393adb7 100644 --- a/src/app/room/[id]/page.tsx +++ b/src/app/room/[id]/page.tsx @@ -31,6 +31,7 @@ export default function RoomPage() { } = useRoomPolling(roomId); useEffect(() => { + if (!roomId) return; const id = getUserId(); setUserId(id); diff --git a/src/components/BlindboxPlanShareCard.tsx b/src/components/BlindboxPlanShareCard.tsx index dc4ff8b..754c233 100644 --- a/src/components/BlindboxPlanShareCard.tsx +++ b/src/components/BlindboxPlanShareCard.tsx @@ -10,9 +10,11 @@ export interface PlanShareData { export default function BlindboxPlanShareCard({ data, cardRef, + bgDataUrl, }: { data: PlanShareData; cardRef: React.RefObject; + bgDataUrl?: string | null; }) { const { days, roomName } = data; const shareUrl = @@ -38,6 +40,22 @@ export default function BlindboxPlanShareCard({ overflow: "hidden", }} > + {/* Background image */} + {bgDataUrl && ( + + )} + {/* Decorative glows */}
; + bgDataUrl?: string | null; }) { const { idea, submitter, drawer, roomName } = data; const shareUrl = @@ -39,6 +41,22 @@ export default function BlindboxShareCard({ overflow: "hidden", }} > + {/* Background image */} + {bgDataUrl && ( + + )} + {/* Decorative glows */}
- -
- - + {image ? ( + + ) : ( + +
+ + + )}

{title}

{subtitle && ( diff --git a/src/components/ProfileFavoritesCard.tsx b/src/components/ProfileFavoritesCard.tsx index 72fae12..d1be85e 100644 --- a/src/components/ProfileFavoritesCard.tsx +++ b/src/components/ProfileFavoritesCard.tsx @@ -71,6 +71,7 @@ export default function ProfileFavoritesCard({ ) : favorites.length === 0 ? ( - + {alt}
); } diff --git a/src/components/RestaurantShareCard.tsx b/src/components/RestaurantShareCard.tsx index 111ac20..c35a31d 100644 --- a/src/components/RestaurantShareCard.tsx +++ b/src/components/RestaurantShareCard.tsx @@ -16,10 +16,12 @@ export default function RestaurantShareCard({ data, cardRef, imageDataUrl, + bgDataUrl, }: { data: RestaurantShareData; cardRef: React.RefObject; imageDataUrl: string | null; + bgDataUrl?: string | null; }) { const { restaurant, matchType, matchLikes, userCount, scene } = data; const isUnanimous = matchType === "unanimous"; @@ -53,6 +55,22 @@ export default function RestaurantShareCard({ overflow: "hidden", }} > + {/* Background image */} + {bgDataUrl && ( + + )} + {/* Decorative glows */}
(null); const [imageLoading, setImageLoading] = useState(false); + const [bgDataUrl, setBgDataUrl] = useState(null); + const imageSrc = data.type === "restaurant" ? data.restaurant.images?.[0] : undefined; + const bgSrc = + data.type === "restaurant" + ? "/share-bg-panic.png" + : data.type === "blindbox" || data.type === "plan" + ? "/share-bg-blindbox.png" + : undefined; useEffect(() => { if (!open) { setImageDataUrl(null); + setBgDataUrl(null); return; } - if (!imageSrc) return; - setImageLoading(true); - loadImageAsDataUrl(imageSrc) - .then(setImageDataUrl) - .finally(() => setImageLoading(false)); - }, [open, imageSrc]); + const promises: Promise[] = []; + + if (imageSrc) { + promises.push( + loadImageAsDataUrl(imageSrc).then(setImageDataUrl), + ); + } + if (bgSrc) { + promises.push( + loadImageAsDataUrl(bgSrc).then(setBgDataUrl), + ); + } + + if (promises.length > 0) { + setImageLoading(true); + Promise.all(promises).finally(() => setImageLoading(false)); + } + }, [open, imageSrc, bgSrc]); const handleGenerate = useCallback(async (): Promise => { if (!cardRef.current) return null; @@ -153,11 +174,12 @@ export default function ShareCardModal({ data={data} cardRef={cardRef} imageDataUrl={imageDataUrl} + bgDataUrl={bgDataUrl} /> ) : data.type === "plan" ? ( - + ) : ( - + )}
diff --git a/src/components/SwipeDeck.tsx b/src/components/SwipeDeck.tsx index 139a722..819d466 100644 --- a/src/components/SwipeDeck.tsx +++ b/src/components/SwipeDeck.tsx @@ -216,7 +216,7 @@ export default function SwipeDeck({ [], ); - const sendSwipe = async (restaurantId: string, action: "like" | "nope") => { + const sendSwipe = async (restaurantId: string, action: "like" | "pass") => { try { const res = await fetch(`/api/room/${roomId}/swipe`, { method: "POST", @@ -240,7 +240,7 @@ export default function SwipeDeck({ swipingRef.current = false; if (guideVisible) setGuideVisible(false); - const action = direction === "right" ? "like" : "nope"; + const action = direction === "right" ? "like" : "pass"; sendSwipe(current.id, action); setSwipeHistory((h) => [...h, current.id]); @@ -291,12 +291,16 @@ export default function SwipeDeck({ prevLikeCounts.current = {}; }, []); + const currentIndexRef = useRef(currentIndex); + currentIndexRef.current = currentIndex; + useEffect(() => { const serverIndex = swipeCounts[userId] ?? 0; - if (serverIndex === 0 && currentIndex > 0 && !resetting) { + if (serverIndex === 0 && currentIndexRef.current > 0 && !resetting) { clearLocalState(); } - }, [swipeCounts, userId, currentIndex, resetting, clearLocalState]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [swipeCounts, userId, resetting, clearLocalState]); const handleReset = useCallback(async () => { setResetting(true); @@ -354,7 +358,7 @@ export default function SwipeDeck({ const isTop = index === currentIndex; return ( diff --git a/src/hooks/useRoomPolling.ts b/src/hooks/useRoomPolling.ts index 70d1de8..b2a76e7 100644 --- a/src/hooks/useRoomPolling.ts +++ b/src/hooks/useRoomPolling.ts @@ -13,9 +13,9 @@ async function fetcher(url: string) { return r.json(); } -export function useRoomPolling(roomId: string) { +export function useRoomPolling(roomId: string | undefined) { const { data, error, isLoading, mutate } = useSWR( - `/api/room/${roomId}`, + roomId ? `/api/room/${roomId}` : null, fetcher, { revalidateOnFocus: true, @@ -27,6 +27,7 @@ export function useRoomPolling(roomId: string) { const notFound = error?.message === "NOT_FOUND"; useEffect(() => { + if (!roomId) return; const es = new EventSource(`/api/room/${roomId}/events`); es.onmessage = (e) => {